1 /*
2     SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 #include "compositionmodel.hpp"
6 #include "assets/keyframes/model/keyframemodellist.hpp"
7 #include "timelinemodel.hpp"
8 #include "trackmodel.hpp"
9 #include "transitions/transitionsrepository.hpp"
10 #include <QDebug>
11 #include <mlt++/MltTransition.h>
12 #include <utility>
13 
CompositionModel(std::weak_ptr<TimelineModel> parent,std::unique_ptr<Mlt::Transition> transition,int id,const QDomElement & transitionXml,const QString & transitionId,const QString & originalDecimalPoint)14 CompositionModel::CompositionModel(std::weak_ptr<TimelineModel> parent, std::unique_ptr<Mlt::Transition> transition, int id, const QDomElement &transitionXml,
15                                    const QString &transitionId, const QString &originalDecimalPoint)
16     : MoveableItem<Mlt::Transition>(std::move(parent), id)
17     , AssetParameterModel(std::move(transition), transitionXml, transitionId, {ObjectType::TimelineComposition, m_id}, originalDecimalPoint)
18     , m_a_track(-1)
19     , m_duration(0)
20 {
21     m_compositionName = TransitionsRepository::get()->getName(transitionId);
22 }
23 
construct(const std::weak_ptr<TimelineModel> & parent,const QString & transitionId,const QString & originalDecimalPoint,int id,std::unique_ptr<Mlt::Properties> sourceProperties)24 int CompositionModel::construct(const std::weak_ptr<TimelineModel> &parent, const QString &transitionId, const QString &originalDecimalPoint, int id,
25                                 std::unique_ptr<Mlt::Properties> sourceProperties)
26 {
27     std::unique_ptr<Mlt::Transition> transition = TransitionsRepository::get()->getTransition(transitionId);
28     transition->set_in_and_out(0, 0);
29     auto xml = TransitionsRepository::get()->getXml(transitionId);
30     if (sourceProperties) {
31         // Paste parameters from existing source composition
32         QStringList sourceProps;
33         for (int i = 0; i < sourceProperties->count(); i++) {
34             sourceProps << sourceProperties->get_name(i);
35         }
36         QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter"));
37         for (int i = 0; i < params.count(); ++i) {
38             QDomElement currentParameter = params.item(i).toElement();
39             QString paramName = currentParameter.attribute(QStringLiteral("name"));
40             if (!sourceProps.contains(paramName)) {
41                 continue;
42             }
43             QString paramValue = sourceProperties->get(paramName.toUtf8().constData());
44             currentParameter.setAttribute(QStringLiteral("value"), paramValue);
45         }
46         if (sourceProps.contains(QStringLiteral("force_track"))) {
47             transition->set("force_track", sourceProperties->get_int("force_track"));
48         }
49     }
50     std::shared_ptr<CompositionModel> composition(new CompositionModel(parent, std::move(transition), id, xml, transitionId, originalDecimalPoint));
51     id = composition->m_id;
52 
53     if (auto ptr = parent.lock()) {
54         ptr->registerComposition(composition);
55     } else {
56         qDebug() << "Error : construction of composition failed because parent timeline is not available anymore";
57         Q_ASSERT(false);
58     }
59 
60     return id;
61 }
62 
requestResize(int size,bool right,Fun & undo,Fun & redo,bool logUndo,bool hasMix)63 bool CompositionModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo, bool hasMix)
64 {
65     Q_UNUSED(hasMix);
66     QWriteLocker locker(&m_lock);
67     if (size <= 0) {
68         return false;
69     }
70     int delta = getPlaytime() - size;
71     qDebug() << "compo request resize to " << size << ", ACTUAL SZ: " << getPlaytime() << ", " << right << delta;
72     int in = getIn();
73     int out = in + getPlaytime() - 1;
74     int oldDuration = out - in;
75     int old_in = in, old_out = out;
76     if (right) {
77         out -= delta;
78     } else {
79         in += delta;
80     }
81     // if the in becomes negative, we add the necessary length in out.
82     if (in < 0) {
83         out = out - in;
84         in = 0;
85     }
86 
87     std::function<bool(void)> track_operation = []() { return true; };
88     std::function<bool(void)> track_reverse = []() { return true; };
89     if (m_currentTrackId != -1) {
90         if (auto ptr = m_parent.lock()) {
91             if (ptr->getTrackById(m_currentTrackId)->isLocked()) {
92                 return false;
93             }
94             track_operation = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, in, out, logUndo);
95         } else {
96             qDebug() << "Error : Moving composition failed because parent timeline is not available anymore";
97             Q_ASSERT(false);
98         }
99     } else {
100         // Perform resize only
101         setInOut(in, out);
102     }
103     QVector<int> roles{TimelineModel::DurationRole};
104     if (!right) {
105         roles.push_back(TimelineModel::StartRole);
106     }
107     Fun refresh = []() { return true; };
108     if (m_assetId == QLatin1String("slide")) {
109         // Slide composition uses a keyframe at end of composition, so update last keyframe
110         refresh = [this]() {
111             QString animation(m_asset->get("rect"));
112             if (animation.contains(QLatin1Char(';')) && !animation.contains(QLatin1String(";-1="))) {
113                 QString result = animation.section(QLatin1Char(';'), 0, 0);
114                 result.append(QStringLiteral(";-1="));
115                 result.append(animation.section(QLatin1Char('='), -1));
116                 m_asset->set("rect", result.toUtf8().constData());
117             }
118             return true;
119         };
120         refresh();
121     } else if (m_assetId == QLatin1String("wipe")) {
122         // Slide composition uses a keyframe at end of composition, so update last keyframe
123         refresh = [this]() {
124             QString animation(m_asset->get("geometry"));
125             if ((animation.contains(QLatin1Char(';')) && !animation.contains(QLatin1String(";-1="))) || animation.endsWith(QLatin1Char('='))) {
126                 if (animation.contains(QLatin1String(" 0%;")) || animation.contains(QLatin1String(" 0;"))) {
127                     // reverse anim
128                     m_asset->set("geometry", "0=0% 0% 100% 100% 0%;-1=0% 0% 100% 100% 100%");
129                 } else {
130                     m_asset->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
131                 }
132             }
133             return true;
134         };
135         refresh();
136     }
137     Fun operation = [this, track_operation, roles]() {
138         if (track_operation()) {
139             // we send a list of roles to be updated
140             if (m_currentTrackId != -1) {
141                 if (auto ptr = m_parent.lock()) {
142                     QModelIndex ix = ptr->makeCompositionIndexFromID(m_id);
143                     ptr->notifyChange(ix, ix, roles);
144                 }
145             }
146             return true;
147         }
148         return false;
149     };
150     if (operation()) {
151         // 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
152         UPDATE_UNDO_REDO(refresh, refresh, undo, redo);
153         if (m_currentTrackId != -1) {
154             if (auto ptr = m_parent.lock()) {
155                 track_reverse = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, old_in, old_out, logUndo);
156             }
157         }
158         Fun reverse = [this, track_reverse, roles]() {
159             if (track_reverse()) {
160                 if (m_currentTrackId != -1) {
161                     if (auto ptr = m_parent.lock()) {
162                         QModelIndex ix = ptr->makeCompositionIndexFromID(m_id);
163                         ptr->notifyChange(ix, ix, roles);
164                     }
165                 }
166                 return true;
167             }
168             return false;
169         };
170 
171         if (logUndo) {
172             auto kfr = getKeyframeModel();
173             if (kfr) {
174                 // Adjust keyframe length
175                 if (oldDuration > 0) {
176                     kfr->resizeKeyframes(0, oldDuration, 0, out - in, 0, right, undo, redo);
177                 }
178                 Fun refresh = [kfr]() {
179                     emit kfr->modelChanged();
180                     return true;
181                 };
182                 refresh();
183                 UPDATE_UNDO_REDO(refresh, refresh, undo, redo);
184             }
185         }
186         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
187         return true;
188     }
189     return false;
190 }
191 
getProperty(const QString & name) const192 const QString CompositionModel::getProperty(const QString &name) const
193 {
194     READ_LOCK();
195     return QString::fromUtf8(service()->get(name.toUtf8().constData()));
196 }
197 
service() const198 Mlt::Transition *CompositionModel::service() const
199 {
200     READ_LOCK();
201     return static_cast<Mlt::Transition *>(m_asset.get());
202 }
203 
properties()204 Mlt::Properties *CompositionModel::properties()
205 {
206     READ_LOCK();
207     return new Mlt::Properties(m_asset.get()->get_properties());
208 }
209 
getPlaytime() const210 int CompositionModel::getPlaytime() const
211 {
212     READ_LOCK();
213     return m_duration + 1;
214 }
215 
getATrack() const216 int CompositionModel::getATrack() const
217 {
218     READ_LOCK();
219     return m_a_track == -1 ? -1 : service()->get_int("a_track");
220 }
221 
setForceTrack(bool force)222 void CompositionModel::setForceTrack(bool force)
223 {
224     READ_LOCK();
225     service()->set("force_track", force ? 1 : 0);
226 }
227 
getForcedTrack() const228 int CompositionModel::getForcedTrack() const
229 {
230     QWriteLocker locker(&m_lock);
231     return (service()->get_int("force_track") == 0 || m_a_track == -1) ? -1 : service()->get_int("a_track");
232 }
233 
setATrack(int trackMltPosition,int trackId)234 void CompositionModel::setATrack(int trackMltPosition, int trackId)
235 {
236     QWriteLocker locker(&m_lock);
237     Q_ASSERT(trackId != getCurrentTrackId()); // can't compose with same track
238     m_a_track = trackMltPosition;
239     if (m_a_track >= 0) {
240         service()->set("a_track", trackMltPosition);
241     }
242     if (m_currentTrackId != -1) {
243         emit compositionTrackChanged();
244     }
245 }
246 
getEffectKeyframeModel()247 KeyframeModel *CompositionModel::getEffectKeyframeModel()
248 {
249     prepareKeyframes();
250     std::shared_ptr<KeyframeModelList> listModel = getKeyframeModel();
251     if (listModel) {
252         return listModel->getKeyModel();
253     }
254     return nullptr;
255 }
256 
showKeyframes() const257 bool CompositionModel::showKeyframes() const
258 {
259     READ_LOCK();
260     return !service()->get_int("kdenlive:hide_keyframes");
261 }
262 
setShowKeyframes(bool show)263 void CompositionModel::setShowKeyframes(bool show)
264 {
265     QWriteLocker locker(&m_lock);
266     service()->set("kdenlive:hide_keyframes", !show);
267 }
268 
displayName() const269 const QString &CompositionModel::displayName() const
270 {
271     return m_compositionName;
272 }
273 
setInOut(int in,int out)274 void CompositionModel::setInOut(int in, int out)
275 {
276     MoveableItem::setInOut(in, out);
277     m_duration = out - in;
278     setPosition(in);
279 }
280 
setGrab(bool grab)281 void CompositionModel::setGrab(bool grab)
282 {
283     QWriteLocker locker(&m_lock);
284     if (grab == m_grabbed) {
285         return;
286     }
287     m_grabbed = grab;
288     if (auto ptr = m_parent.lock()) {
289         QModelIndex ix = ptr->makeCompositionIndexFromID(m_id);
290         emit ptr->dataChanged(ix, ix, {TimelineModel::GrabbedRole});
291     }
292 }
293 
setSelected(bool sel)294 void CompositionModel::setSelected(bool sel)
295 {
296     QWriteLocker locker(&m_lock);
297     if (sel == selected) {
298         return;
299     }
300     selected = sel;
301     if (auto ptr = m_parent.lock()) {
302         if (m_currentTrackId != -1) {
303             QModelIndex ix = ptr->makeCompositionIndexFromID(m_id);
304             emit ptr->dataChanged(ix, ix, {TimelineModel::SelectedRole});
305         }
306     }
307 }
308 
setCurrentTrackId(int tid,bool finalMove)309 void CompositionModel::setCurrentTrackId(int tid, bool finalMove)
310 {
311     Q_UNUSED(finalMove);
312     MoveableItem::setCurrentTrackId(tid);
313 }
314 
getOut() const315 int CompositionModel::getOut() const
316 {
317     return getPosition() + m_duration;
318 }
319 
getIn() const320 int CompositionModel::getIn() const
321 {
322     return getPosition();
323 }
324 
toXml(QDomDocument & document)325 QDomElement CompositionModel::toXml(QDomDocument &document)
326 {
327     QDomElement container = document.createElement(QStringLiteral("composition"));
328     container.setAttribute(QStringLiteral("id"), m_id);
329     container.setAttribute(QStringLiteral("composition"), m_assetId);
330     container.setAttribute(QStringLiteral("in"), getIn());
331     container.setAttribute(QStringLiteral("out"), getOut());
332     container.setAttribute(QStringLiteral("position"), getPosition());
333     if (auto ptr = m_parent.lock()) {
334         int trackId = ptr->getTrackPosition(m_currentTrackId);
335         container.setAttribute(QStringLiteral("track"), trackId);
336     }
337     container.setAttribute(QStringLiteral("a_track"), getATrack());
338     QScopedPointer<Mlt::Properties> props(properties());
339     for (int i = 0; i < props->count(); i++) {
340         QString name = props->get_name(i);
341         if (name.startsWith(QLatin1Char('_'))) {
342             continue;
343         }
344         Xml::setXmlProperty(container, name, props->get(i));
345     }
346     return container;
347 }
348