Complete rewrite of copy/paste in timeline. Now much more robust.

parent 3e8d10d9
......@@ -159,6 +159,73 @@ bool EffectStackModel::copyEffect(std::shared_ptr<AbstractEffectItem> sourceItem
return result;
}
QDomElement EffectStackModel::toXml(QDomDocument &document)
{
QDomElement container = document.createElement(QStringLiteral("effects"));
for (int i = 0; i < rootItem->childCount(); ++i) {
std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(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 <QPair<QString,QVariant> > params = sourceEffect->getAllParameters();
QLocale locale;
for (auto param : 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<QPair<QString, QVariant>> parameters;
QDomNodeList params = node.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < params.count(); j++) {
QDomElement pnode = params.item(j).toElement();
parameters.append(QPair<QString, QVariant>(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());
connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
fadeIns.insert(effect->getId());
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
fadeOuts.insert(effect->getId());
}
local_redo();
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
}
if (true) {
Fun update = [this]() {
emit dataChanged(QModelIndex(), QModelIndex(), QVector<int>());
return true;
};
update();
PUSH_LAMBDA(update, redo);
PUSH_LAMBDA(update, undo);
}
}
bool EffectStackModel::copyEffect(std::shared_ptr<AbstractEffectItem> sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
......
......@@ -125,6 +125,11 @@ public:
bool isStackEnabled() const;
/* @brief Returns an XML representation of the effect stack with all parameters */
QDomElement toXml(QDomDocument &document);
/* @brief Load an effect stack from an XML representation */
void fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo);
public slots:
/* @brief Delete an effect from the stack */
void removeEffect(std::shared_ptr<EffectItemModel> effect);
......
......@@ -480,7 +480,6 @@ void MainWindow::init()
connect(m_transitionsMenu, &QMenu::triggered, this, &MainWindow::slotAddTransition);
m_timelineContextMenu = new QMenu(this);
m_timelineContextTransitionMenu = new QMenu(this);
m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("insert_space")));
m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space")));
......@@ -494,11 +493,6 @@ void MainWindow::init()
m_timelineClipActions->addMenu(m_transitionsMenu);
m_timelineClipActions->addMenu(m_effectsMenu);*/
m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("delete_timeline_clip")));
m_timelineContextTransitionMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Copy)));
m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("auto_transition")));
slotConnectMonitors();
m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
......
......@@ -190,7 +190,6 @@ private:
QMenu *m_transitionsMenu;
QMenu *m_timelineContextMenu;
QList <QAction *> m_timelineClipActions;
QMenu *m_timelineContextTransitionMenu;
KDualAction *m_useTimelineZone;
/** Action names that can be used in the slotDoAction() slot, with their i18n() names */
......
......@@ -599,3 +599,16 @@ 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());
container.setAttribute(QStringLiteral("track"), getCurrentTrackId());
container.appendChild(m_effectStack->toXml(document));
return container;
}
......@@ -100,6 +100,9 @@ public:
int getFakePosition() const;
void setFakePosition(int fid);
/* @brief Returns an XML representation of the clip with its effects */
QDomElement toXml(QDomDocument &document);
protected:
// helper fuctions that creates the lambda
Fun setClipState_lambda(PlaylistState::ClipState state);
......
......@@ -725,6 +725,16 @@ const QString GroupsModel::toJson() const
return QString(json.toJson());
}
const QString GroupsModel::toJson(std::unordered_set<int> 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"))) {
......@@ -801,6 +811,61 @@ bool GroupsModel::fromJson(const QString &data)
return ok;
}
bool GroupsModel::fromJsonWithOffset(const QString &data, QMap<int, int> trackMap, int offset)
{
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;
}
auto list = json.array();
bool ok = true;
qDebug()<<"* * *READY TO LOAD JSON DATA";
for (auto elem : list) {
qDebug()<<"* * *LOADING GROUP + + + + + + ++ + + +";
if (!elem.isObject()) {
qDebug() << "Error : Expected json object while parsing groups";
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<int> 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 data = child.value(QLatin1String("data")).toString();
int trackId = ptr->getTrackIndexFromPosition(data.section(":", 0, 0).toInt());
int pos = data.section(":", 1, 1).toInt();
qDebug()<<"// ORIGINAL GROUP DATA: "<<trackId<<" / "<<pos;
trackId = ptr->getTrackMltIndex(trackMap.value(trackId)) - 1;
pos += offset;
child.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(trackId).arg(pos)));
qDebug()<<"// UPDATING GROUP DATA: "<<trackId<<" / "<<pos;
}
updatedNodes.append(QJsonValue(child));
}
}
qDebug()<<"* ** * UPDATED JSON NODES: "<<updatedNodes;
obj.insert(QLatin1String("children"), QJsonValue(updatedNodes));
qDebug()<<"* ** * UPDATED JSON NODES: "<<obj;
ok = ok && fromJson(obj, undo, redo);
}
return ok;
}
void GroupsModel::setType(int gid, GroupType type)
{
Q_ASSERT(m_groupIds.count(gid) != 0);
......
......@@ -159,7 +159,9 @@ public:
To workaround that, we currently identify clips by their position + track
*/
const QString toJson() const;
const QString toJson(std::unordered_set<int> roots) const;
bool fromJson(const QString &data);
bool fromJsonWithOffset(const QString &data, QMap<int, int> trackMap, int offset);
/* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */
int getSplitPartner(int id) const;
......
......@@ -872,3 +872,46 @@ void TimelineFunctions::saveTimelineSelection(std::shared_ptr<TimelineItemModel>
xmlConsumer.connect(newTractor);
xmlConsumer.run();
}
int TimelineFunctions::getTrackOffset(std::shared_ptr<TimelineItemModel> 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()<<"+ + +TRSTING TRACK: "<<track;
int trackId = timeline->getTrackIndexFromPosition(track - 1);
if (isAudio == timeline->isAudioTrack(trackId)) {
offset ++;
}
}
return offset;
}
int TimelineFunctions::getOffsetTrackId(std::shared_ptr<TimelineItemModel> 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;
int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
if (timeline->isAudioTrack(trackId) == isAudio) {
offset += offset > 0 ? -1 : 1;
}
}
return timeline->getTrackIndexFromPosition(masterTrackMltIndex -1);
}
......@@ -93,6 +93,12 @@ struct TimelineFunctions
static void setCompositionATrack(std::shared_ptr<TimelineItemModel> timeline, int cid, int aTrack);
static void enableMultitrackView(std::shared_ptr<TimelineItemModel> timeline, bool enable);
static void saveTimelineSelection(std::shared_ptr<TimelineItemModel> timeline, QList <int> selection, QDir targetDir);
/** @brief returns the number of same type tracks between 2 tracks
*/
static int getTrackOffset(std::shared_ptr<TimelineItemModel> timeline, int startTrack, int destTrack);
/** @brief returns an offset track id
*/
static int getOffsetTrackId(std::shared_ptr<TimelineItemModel> timeline, int startTrack, int offset, bool audioOffset);
};
#endif
......@@ -384,6 +384,14 @@ int TimelineModel::getMirrorVideoTrackId(int trackId) const
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();
......
......@@ -319,6 +319,7 @@ public:
/* @brief Retuns 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
......
......@@ -486,8 +486,63 @@ void TimelineController::slotUpdateSelection(int itemId)
void TimelineController::copyItem()
{
int clipId = -1;
int masterTrack = -1;
if (!m_selection.selectedItems.isEmpty()) {
clipId = m_selection.selectedItems.first();
// Check grouped clips
QList <int> extraClips = m_selection.selectedItems;
masterTrack = m_model->getItemTrackId(m_selection.selectedItems.first());
std::unordered_set<int> 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<int> selection = m_model->m_groups->getDirectChildren(gid);
for (int j : selection) {
if (groupRoots.find(j) == groupRoots.end()) {
groupRoots.insert(j);
}
}
}
std::unordered_set<int> selection = m_model->getGroupElements(id);
for (int j : selection) {
if (m_model->isClip(j) && !extraClips.contains(j)) {
extraClips << j;
}
}
}
}
qDebug()<<"==============\n GROUP ROOTS: ";
for (int gp : groupRoots) {
qDebug()<<"GROUP: "<<gp;
}
qDebug()<<"\n=======";
m_copiedItems.clear();
int offset = -1;
QDomElement container = m_copiedItems.createElement(QStringLiteral("list"));
m_copiedItems.appendChild(container);
for (int id : extraClips) {
if (m_model->isClip(id)) {
if (offset == -1 || m_model->getClipPosition(id) < offset) {
offset = m_model->getClipPosition(id);
}
container.appendChild(m_model->m_allClips[id]->toXml(m_copiedItems));
}
}
container.setAttribute(QStringLiteral("offset"), offset);
container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
QDomElement grp = m_copiedItems.createElement(QStringLiteral("groups"));
container.appendChild(grp);
grp.appendChild(m_copiedItems.createTextNode(m_model->m_groups->toJson(groupRoots)));
//TODO: groups
qDebug()<<" / // / PASTED DOC: \n\n"<<m_copiedItems.toString()<<"\n\n------------";
} else {
return;
}
......@@ -513,8 +568,60 @@ bool TimelineController::pasteItem(int clipId, int tid, int position)
if (position == -1) {
position = timelinePosition();
}
qDebug() << "PASTING CLIP: " << clipId << ", " << tid << ", " << position;
return TimelineFunctions::requestItemCopy(m_model, clipId, tid, position);
QDomNodeList clips = m_copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
int offset = m_copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
int masterTrack = m_copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt();
int trackOffset = TimelineFunctions::getTrackOffset(m_model, masterTrack, tid);
bool masterIsAudio = m_model->isAudioTrack(masterTrack);
// find paste tracks
QMap <int, int> tracksMap;
for (int i = 0; i < clips.count(); i++) {
QDomElement prod = clips.at(i).toElement();
int trackId = 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));
}
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
bool res = true;
QMap <int, int> correspondingIds;
for (int i = 0; res && i < clips.count(); i++) {
QDomElement prod = clips.at(i).toElement();
QString originalId = prod.attribute(QStringLiteral("binid"));
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int trackId = prod.attribute(QStringLiteral("track")).toInt();
int mltTrackIndex = m_model->getTrackMltIndex(trackId);
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
int newId;
res = m_model->requestClipCreation(originalId, newId, m_model->getTrackById_const(trackId)->trackType(), undo, redo);
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<EffectStackModel> destStack = m_model->getClipEffectStackModel(newId);
destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo);
}
}
if (!res) {
undo();
return false;
}
const QString groupsData = m_copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
qDebug()<<"************** GRP DATA ********\n"<<groupsData<<"\n******";
m_model->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset);
pCore->pushUndo(undo, redo, i18n("Paste clips"));
return true;
//return TimelineFunctions::requestItemCopy(m_model, clipId, tid, position);
return false;
}
......
......@@ -473,6 +473,7 @@ private:
void initializePreview();
// Get a list of currently selected items, including clips grouped with selection
std::unordered_set<int> getCurrentSelectionIds() const;
QDomDocument m_copiedItems;
signals:
void selected(Mlt::Producer *producer);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment