groupsmodel.cpp 37.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/***************************************************************************
 *   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 <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
23
#include "macros.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
24
#include "timelineitemmodel.hpp"
25
#include "trackmodel.hpp"
26
#include <QDebug>
27
28
29
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
30
#include <QModelIndex>
31
#include <queue>
32
#include <stack>
Nicolas Carion's avatar
linting    
Nicolas Carion committed
33
#include <utility>
34

35
36
GroupsModel::GroupsModel(std::weak_ptr<TimelineItemModel> parent)
    : m_parent(std::move(parent))
Nicolas Carion's avatar
Nicolas Carion committed
37
    , m_lock(QReadWriteLock::Recursive)
38
39
40
{
}

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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);
    }
}

71
Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set<int> &ids, GroupType type, int parent)
72
{
Nicolas Carion's avatar
Nicolas Carion committed
73
    QWriteLocker locker(&m_lock);
74
    Q_ASSERT(ids.size() == 0 || type != GroupType::Leaf);
75
    return [gid, ids, parent, type, this]() {
76
        createGroupItem(gid);
77
78
79
        if (parent != -1) {
            setGroup(gid, parent);
        }
80

81
82
83
84
        if (ids.size() > 0) {
            promoteToGroup(gid, type);
            std::unordered_set<int> roots;
            std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); });
85
86
            auto ptr = m_parent.lock();
            if (!ptr) Q_ASSERT(false);
87
            for (int id : roots) {
88
89
90
91
92
93
                if (type != GroupType::Selection) {
                    setGroup(getRootId(id), gid, true);
                } else {
                    setGroup(getRootId(id), gid, false);
                    ptr->setSelected(id, true);
                }
94
            }
95
96
97
98
99
        }
        return true;
    };
}

100
int GroupsModel::groupItems(const std::unordered_set<int> &ids, Fun &undo, Fun &redo, GroupType type, bool force)
101
{
Nicolas Carion's avatar
Nicolas Carion committed
102
    QWriteLocker locker(&m_lock);
103
    Q_ASSERT(type != GroupType::Leaf);
Nicolas Carion's avatar
Nicolas Carion committed
104
    Q_ASSERT(!ids.empty());
105
106
107
    std::unordered_set<int> roots;
    std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); });
    if (roots.size() == 1 && !force) {
108
        // We do not create a group with only one element. Instead, we return the id of that element
109
        return *(roots.begin());
110
    }
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    if (type == GroupType::AVSplit && !force) {
        // additional checks for AVSplit
        if (roots.size() != 2) {
            // must group exactly two items
            return -1;
        }
        auto it = roots.begin();
        int cid1 = *it;
        ++it;
        int cid2 = *it;
        auto ptr = m_parent.lock();
        if (!ptr) Q_ASSERT(false);
        if (cid1 == cid2 || !ptr->isClip(cid1) || !ptr->isClip(cid2)) {
            // invalid: we must get two different clips
            return -1;
        }
        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) {
            // invalid: we must insert one in video the other in audio
            return -1;
        }
    }
136
    int gid = TimelineModel::getNextId();
137
    auto operation = groupItems_lambda(gid, roots, type);
138
    if (operation()) {
139
        auto reverse = destructGroupItem_lambda(gid);
140
141
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
        return gid;
142
    }
143
    return -1;
144
145
}

146
bool GroupsModel::ungroupItem(int id, Fun &undo, Fun &redo)
147
{
Nicolas Carion's avatar
Nicolas Carion committed
148
    QWriteLocker locker(&m_lock);
149
    int gid = getRootId(id);
150
    if (m_groupIds.count(gid) == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
151
        // element is not part of a group
152
        return false;
153
    }
154
155

    return destructGroupItem(gid, true, undo, redo);
156
157
}

158
159
void GroupsModel::createGroupItem(int id)
{
Nicolas Carion's avatar
Nicolas Carion committed
160
    QWriteLocker locker(&m_lock);
161
162
163
164
165
166
    Q_ASSERT(m_upLink.count(id) == 0);
    Q_ASSERT(m_downLink.count(id) == 0);
    m_upLink[id] = -1;
    m_downLink[id] = std::unordered_set<int>();
}

167
Fun GroupsModel::destructGroupItem_lambda(int id)
168
{
Nicolas Carion's avatar
Nicolas Carion committed
169
    QWriteLocker locker(&m_lock);
170
    return [this, id]() {
171
        removeFromGroup(id);
172
173
        auto ptr = m_parent.lock();
        if (!ptr) Q_ASSERT(false);
174
175
        for (int child : m_downLink[id]) {
            m_upLink[child] = -1;
176
            QModelIndex ix;
177
            if (ptr->isClip(child)) {
178
179
180
181
182
                ix = ptr->makeClipIndexFromID(child);
            } else if (ptr->isComposition(child)) {
                ix = ptr->makeCompositionIndexFromID(child);
            }
            if (ix.isValid()) {
183
184
                ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole});
            }
185
        }
186
187
188
189
        m_downLink[id].clear();
        if (getType(id) != GroupType::Leaf) {
            downgradeToLeaf(id);
        }
190
191
192
193
194
195
        m_downLink.erase(id);
        m_upLink.erase(id);
        return true;
    };
}

196
bool GroupsModel::destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo)
197
{
Nicolas Carion's avatar
Nicolas Carion committed
198
    QWriteLocker locker(&m_lock);
199
    Q_ASSERT(m_upLink.count(id) > 0);
200
    int parent = m_upLink[id];
201
    auto old_children = m_downLink[id];
202
203
204
205
    auto old_type = getType(id);
    auto old_parent_type = GroupType::Normal;
    if (parent != -1) {
        old_parent_type = getType(parent);
206
    }
207
    auto operation = destructGroupItem_lambda(id);
208
    if (operation()) {
209
        auto reverse = groupItems_lambda(id, old_children, old_type, parent);
210
211
212
213
214
215
216
217
        // 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);
        }
218
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
219
        if (parent != -1 && m_downLink[parent].empty() && deleteOrphan) {
220
            return destructGroupItem(parent, true, undo, redo);
221
        }
222
        return true;
223
    }
224
    return false;
225
226
}

227
228
bool GroupsModel::destructGroupItem(int id)
{
Nicolas Carion's avatar
Nicolas Carion committed
229
    QWriteLocker locker(&m_lock);
230
    return destructGroupItem_lambda(id)();
231
232
}

233
234
int GroupsModel::getRootId(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
235
    READ_LOCK();
Nicolas Carion's avatar
Nicolas Carion committed
236
    std::unordered_set<int> seen; // we store visited ids to detect cycles
237
238
239
240
241
242
243
244
245
    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;
        }
Nicolas Carion's avatar
Nicolas Carion committed
246
    } while (father != -1);
247
    return id;
248
249
250
251
}

bool GroupsModel::isLeaf(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
252
    READ_LOCK();
253
    Q_ASSERT(m_downLink.count(id) > 0);
Nicolas Carion's avatar
Nicolas Carion committed
254
    return m_downLink.at(id).empty();
255
256
}

257
258
bool GroupsModel::isInGroup(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
259
    READ_LOCK();
260
261
262
263
    Q_ASSERT(m_downLink.count(id) > 0);
    return getRootId(id) != id;
}

264
265
266
267
268
269
270
271
272
273
274
275
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<int> leaves = getDirectChildren(groupId);
    if (leaves.size() != 2) {
        // clip does not have an AV split partner
276
        qDebug() << "WRONG SPLIT GROUP SIZE: " << leaves.size();
277
278
279
280
281
282
283
284
285
286
        return -1;
    }
    for (const int &child : leaves) {
        if (child != id) {
            return child;
        }
    }
    return -1;
}

287
288
std::unordered_set<int> GroupsModel::getSubtree(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
289
    READ_LOCK();
290
291
292
293
294
295
296
    std::unordered_set<int> result;
    result.insert(id);
    std::queue<int> queue;
    queue.push(id);
    while (!queue.empty()) {
        int current = queue.front();
        queue.pop();
Nicolas Carion's avatar
Nicolas Carion committed
297
        for (const int &child : m_downLink.at(current)) {
298
299
300
301
302
303
304
305
306
            result.insert(child);
            queue.push(child);
        }
    }
    return result;
}

std::unordered_set<int> GroupsModel::getLeaves(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
307
    READ_LOCK();
308
309
310
311
312
313
    std::unordered_set<int> result;
    std::queue<int> queue;
    queue.push(id);
    while (!queue.empty()) {
        int current = queue.front();
        queue.pop();
Nicolas Carion's avatar
Nicolas Carion committed
314
        for (const int &child : m_downLink.at(current)) {
315
316
            queue.push(child);
        }
Nicolas Carion's avatar
Nicolas Carion committed
317
        if (m_downLink.at(current).empty()) {
318
319
320
321
322
323
            result.insert(current);
        }
    }
    return result;
}

324
325
std::unordered_set<int> GroupsModel::getDirectChildren(int id) const
{
Nicolas Carion's avatar
Nicolas Carion committed
326
    READ_LOCK();
327
328
329
    Q_ASSERT(m_downLink.count(id) > 0);
    return m_downLink.at(id);
}
330
331
332
333
334
335
int GroupsModel::getDirectAncestor(int id) const
{
    READ_LOCK();
    Q_ASSERT(m_upLink.count(id) > 0);
    return m_upLink.at(id);
}
336

337
void GroupsModel::setGroup(int id, int groupId, bool changeState)
338
{
Nicolas Carion's avatar
Nicolas Carion committed
339
    QWriteLocker locker(&m_lock);
340
    Q_ASSERT(m_upLink.count(id) > 0);
341
    Q_ASSERT(groupId == -1 || m_downLink.count(groupId) > 0);
342
    Q_ASSERT(id != groupId);
343
344
    removeFromGroup(id);
    m_upLink[id] = groupId;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
345
346
    if (groupId != -1) {
        m_downLink[groupId].insert(id);
347
348
349
350
351
352
353
354
355
356
357
358
        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});
            }
        }
Nicolas Carion's avatar
Nicolas Carion committed
359
        if (getType(groupId) == GroupType::Leaf) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
360
361
            promoteToGroup(groupId, GroupType::Normal);
        }
362
    }
363
364
365
366
}

void GroupsModel::removeFromGroup(int id)
{
Nicolas Carion's avatar
Nicolas Carion committed
367
    QWriteLocker locker(&m_lock);
368
369
    Q_ASSERT(m_upLink.count(id) > 0);
    Q_ASSERT(m_downLink.count(id) > 0);
370
371
    int parent = m_upLink[id];
    if (parent != -1) {
372
        Q_ASSERT(getType(parent) != GroupType::Leaf);
373
        m_downLink[parent].erase(id);
374
375
376
377
378
379
380
381
382
383
384
        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});
        }
385
386
387
        if (m_downLink[parent].size() == 0) {
            downgradeToLeaf(parent);
        }
388
389
390
    }
    m_upLink[id] = -1;
}
391

392
bool GroupsModel::mergeSingleGroups(int id, Fun &undo, Fun &redo)
393
{
394
395
    // 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
396
    QWriteLocker locker(&m_lock);
397
    Q_ASSERT(m_upLink.count(id) > 0);
398
399
    auto leaves = getLeaves(id);
    std::unordered_map<int, int> old_parents, new_parents;
400
    std::vector<int> to_delete;
401
    std::unordered_set<int> processed; // to avoid going twice along the same branch
402
    for (int leaf : leaves) {
403
        int current = m_upLink[leaf];
404
        int start = leaf;
405
        while (current != m_upLink[id] && processed.count(current) == 0) {
406
407
408
409
410
411
412
413
414
415
            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;
            }
416
417
            current = m_upLink[current];
        }
418
419
420
        if (current != m_upLink[start]) {
            old_parents[start] = m_upLink[start];
            new_parents[start] = current;
421
422
        }
    }
423
    auto parent_changer = [this](const std::unordered_map<int, int> &parents) {
424
425
426
427
428
        auto ptr = m_parent.lock();
        if (!ptr) {
            qDebug() << "Impossible to create group because the timeline is not available anymore";
            return false;
        }
429
        for (const auto &group : parents) {
430
431
432
433
            setGroup(group.first, group.second);
        }
        return true;
    };
434
435
    Fun reverse = [old_parents, parent_changer]() { return parent_changer(old_parents); };
    Fun operation = [new_parents, parent_changer]() { return parent_changer(new_parents); };
436
437
438
439
440
441
442
443
444
445
    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);
446
447
448
        if (getType(gid) == GroupType::Selection) {
            continue;
        }
449
        res = destructGroupItem(gid, false, undo, redo);
450
451
452
453
454
455
456
457
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return res;
        }
    }
    return true;
}
458

459
bool GroupsModel::split(int id, const std::function<bool(int)> &criterion, Fun &undo, Fun &redo)
460
461
{
    QWriteLocker locker(&m_lock);
462
463
464
    if (isLeaf(id)) {
        return true;
    }
465
466
    // 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);
467
468
    Q_ASSERT(m_groupIds[id] != GroupType::Selection);
    bool regroup = true; // we don't support splitting if selection group is active
469
470
    // We do a BFS on the tree to copy it
    // We store corresponding nodes
471
    std::unordered_map<int, int> corresp; // keys are id in the original tree, values are temporary negative id assigned for creation of the new tree
472
473
    corresp[-1] = -1;
    // These are the nodes to be moved to new tree
474
    std::vector<int> to_move;
475
476
    // 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)
477
    std::unordered_map<int, std::unordered_set<int>> new_groups;
478
479
    // We store also the target type of the new groups
    std::unordered_map<int, GroupType> new_types;
480
481
482
483
484
485
486
487
    std::queue<int> queue;
    queue.push(id);
    int tempId = -10;
    while (!queue.empty()) {
        int current = queue.front();
        queue.pop();
        if (!isLeaf(current) || criterion(current)) {
            if (isLeaf(current)) {
488
489
                to_move.push_back(current);
                new_groups[corresp[m_upLink[current]]].insert(current);
490
491
            } else {
                corresp[current] = tempId;
492
                new_types[tempId] = getType(current);
493
                if (m_upLink[current] != -1) new_groups[corresp[m_upLink[current]]].insert(tempId);
494
495
496
497
498
499
500
501
502
503
                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) {
504
        destructGroupItem(leaf, true, undo, redo);
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
    }

    // 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);

526
527
    // We prune the new_groups to remove empty ones
    bool finished = false;
528
    while (!finished) {
529
530
        finished = true;
        int selected = INT_MAX;
531
        for (const auto &it : new_groups) {
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
            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);
Nicolas Carion's avatar
Nicolas Carion committed
549
550
            for (auto &new_group : new_groups) {
                new_group.second.erase(selected);
551
552
553
            }
        }
    }
554
555
556
557
    // 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<int, int> created_id; // to keep track of node that we create
558

559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
    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;
            }
        }
575
        Q_ASSERT(selected != INT_MAX);
576
577
578
579
        std::unordered_set<int> group;
        for (int elem : new_groups[selected]) {
            group.insert(elem < -1 ? created_id[elem] : elem);
        }
580
        Q_ASSERT(new_types.count(selected) != 0);
581
        int gid = groupItems(group, undo, redo, new_types[selected], true);
582
583
584
585
        created_id[selected] = gid;
        new_groups.erase(selected);
    }

586
    if (regroup) {
587
        if (m_groupIds.count(id) > 0) {
588
            mergeSingleGroups(id, undo, redo);
589
590
        }
        if (created_id[corresp[id]]) {
591
            mergeSingleGroups(created_id[corresp[id]], undo, redo);
592
        }
593
    }
594
595
596
597

    return res;
}

598
void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo)
Nicolas Carion's avatar
Nicolas Carion committed
599
{
600
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
601
    Q_ASSERT(m_upLink.count(targetId) > 0);
602
    Fun operation = [this, id, group = m_upLink[targetId]]() {
603
        setGroup(id, group);
604
605
        return true;
    };
606
    Fun reverse = [this, id, group = m_upLink[id]]() {
607
        setGroup(id, group);
608
609
610
611
        return true;
    };
    operation();
    UPDATE_UNDO_REDO(operation, reverse, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
612
}
Nicolas Carion's avatar
Nicolas Carion committed
613

614
bool GroupsModel::createGroupAtSameLevel(int id, std::unordered_set<int> to_add, GroupType type, Fun &undo, Fun &redo)
615
616
617
618
619
620
621
622
623
624
{
    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<int, int> old_parents;
    to_add.insert(id);
625

626
627
628
629
    for (int g : to_add) {
        Q_ASSERT(m_upLink.count(g) > 0);
        old_parents[g] = m_upLink[g];
    }
630
    Fun operation = [this, gid, type, to_add, parent = m_upLink.at(id)]() {
631
632
633
634
635
636
637
638
        createGroupItem(gid);
        setGroup(gid, parent);
        for (const auto &g : to_add) {
            setGroup(g, gid);
        }
        setType(gid, type);
        return true;
    };
639
    Fun reverse = [this, old_parents, gid]() {
640
641
642
643
644
645
646
647
648
649
650
651
652
653
        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;
}

654
bool GroupsModel::processCopy(int gid, std::unordered_map<int, int> &mapping, Fun &undo, Fun &redo)
Nicolas Carion's avatar
Nicolas Carion committed
655
656
657
658
659
660
661
662
663
{
    qDebug() << "processCopy" << gid;
    if (isLeaf(gid)) {
        qDebug() << "it is a leaf";
        return true;
    }
    bool ok = true;
    std::unordered_set<int> targetGroup;
    for (int child : m_downLink.at(gid)) {
664
        ok = ok && processCopy(child, mapping, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
665
666
667
668
669
670
        if (!ok) {
            break;
        }
        targetGroup.insert(mapping.at(child));
    }
    qDebug() << "processCopy" << gid << "success of child" << ok;
671
    if (ok && m_groupIds[gid] != GroupType::Selection) {
672
        int id = groupItems(targetGroup, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
673
674
675
676
677
678
        qDebug() << "processCopy" << gid << "created id" << id;
        if (id != -1) {
            mapping[gid] = id;
            return true;
        }
    }
679
    return ok;
Nicolas Carion's avatar
Nicolas Carion committed
680
681
}

682
bool GroupsModel::copyGroups(std::unordered_map<int, int> &mapping, Fun &undo, Fun &redo)
Nicolas Carion's avatar
Nicolas Carion committed
683
684
685
686
687
{
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
    // destruct old groups for the targets items
    for (const auto &corresp : mapping) {
688
        ungroupItem(corresp.second, local_undo, local_redo);
Nicolas Carion's avatar
Nicolas Carion committed
689
690
691
692
693
694
695
696
    }
    std::unordered_set<int> 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;
697
        res = res && processCopy(r, mapping, local_undo, local_redo);
Nicolas Carion's avatar
Nicolas Carion committed
698
699
700
701
702
703
704
705
706
        if (!res) {
            bool undone = local_undo();
            Q_ASSERT(undone);
            return false;
        }
    }
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
}
707
708
709

GroupType GroupsModel::getType(int id) const
{
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
    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<int> roots;
    std::transform(m_groupIds.begin(), m_groupIds.end(), std::inserter(roots, roots.begin()),
748
749
750
751
752
753
754
755
                   [&](decltype(*m_groupIds.begin()) g) { 
        const int parentId = getRootId(g.first);
        if (getType(parentId) == GroupType::Selection) {
            // Don't insert selection group, only its child groups
            return g.first;
        }
        return parentId;
    });
756
757
    QJsonArray list;
    for (int r : roots) {
758
        list.push_back(toJson(r));
759
760
761
762
763
    }
    QJsonDocument json(list);
    return QString(json.toJson());
}

764
765
766
767
768
769
770
771
772
773
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());
}

774
int GroupsModel::fromJson(const QJsonObject &o, Fun &undo, Fun &redo)
775
776
{
    if (!o.contains(QLatin1String("type"))) {
Nicolas Carion's avatar
Nicolas Carion committed
777
        qDebug() << "CANNOT PARSE GROUP DATA";
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
        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);
794
            } else if (leaf == QLatin1String("composition")) {
795
                id = ptr->getCompositionByPosition(trackId, pos);
796
            } else {
Nicolas Carion's avatar
Nicolas Carion committed
797
                qDebug() << " * * *UNKNOWN ITEM: " << leaf;
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
            }
            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<int> ids;
        for (const auto &c : children) {
            if (!c.isObject()) {
                qDebug() << "Error : Expected json object while parsing groups";
                return -1;
            }
821
            ids.insert(fromJson(c.toObject(), undo, redo));
822
823
824
825
        }
        if (ids.count(-1) > 0) {
            return -1;
        }
826
        return groupItems(ids, undo, redo, type);
827
828
829
830
831
832
833
834
835
836
837
838
839
    }
    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;
    }
840
    const auto list = json.array();
841
    bool ok = true;
842
    for (const auto &elem : list) {
843
844
845
846
847
        if (!elem.isObject()) {
            qDebug() << "Error : Expected json object while parsing groups";
            undo();
            return false;
        }
848
        ok = ok && fromJson(elem.toObject(), undo, redo);
849
850
    }
    return ok;
851
}
852

853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
void GroupsModel::adjustOffset(QJsonArray &updatedNodes, QJsonObject childObject, int offset, const QMap<int, int> &trackMap)
{
    auto value = childObject.value(QLatin1String("children"));
    auto children = value.toArray();
    for (auto c : children) {
        if (!c.isObject()) {
            continue;
        }
        auto child = c.toObject();
        auto type = groupTypeFromStr(child.value(QLatin1String("type")).toString());
        if (child.contains(QLatin1String("data"))) {
            if (auto ptr = m_parent.lock()) {
                QString cur_data = child.value(QLatin1String("data")).toString();
                int trackId = 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));
            }
        } else if (type != GroupType::Leaf) {
            QJsonObject currentGroup;
            currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(type)));
            QJsonArray array;
            adjustOffset(array, child, offset, trackMap);
            currentGroup.insert(QLatin1String("children"), array);
            updatedNodes.append(QJsonValue(currentGroup));
        }
    }
}

Nicolas Carion's avatar
Nicolas Carion committed
884
bool GroupsModel::fromJsonWithOffset(const QString &data, const QMap<int, int> &trackMap, int offset, Fun &undo, Fun &redo)
885
{
886
887
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
888
889
890
891
892
893
    auto json = QJsonDocument::fromJson(data.toUtf8());
    if (!json.isArray()) {
        qDebug() << "Error : Json file should be an array";
        return false;
    }
    auto list = json.array();
894
    QJsonArray newGroups;
895
896
897
898
    bool ok = true;
    for (auto elem : list) {
        if (!elem.isObject()) {
            qDebug() << "Error : Expected json object while parsing groups";
899
            local_undo();
900
901
902
            return false;
        }
        QJsonObject obj = elem.toObject();
903
904
        QJsonArray updatedNodes;
        auto type = groupTypeFromStr(obj.value(QLatin1String("type")).toString());
905
906
907
908
909
        auto value = obj.value(QLatin1String("children"));
        if (!value.isArray()) {
            qDebug() << "Error : Expected json array of children while parsing groups";
            continue;
        }
910
        // Adjust offset
911
        auto children = value.toArray();
912
913
914
915
916
917
918
919
920
921
922
        adjustOffset(updatedNodes, obj, offset, trackMap);
        QJsonObject currentGroup;
        currentGroup.insert(QLatin1String("children"), QJsonValue(updatedNodes));
        currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(type)));
        newGroups.append(QJsonValue(currentGroup));
    }

    // Group
    for (const auto &elem : newGroups) {
        if (!elem.isObject()) {
            qDebug() << "Error : Expected json object while parsing groups";
923
            break;
924
        }
925
        ok = ok && fromJson(elem.toObject(), local_undo, local_redo);
926
    }
927
    
928
929
930
931
932
933
    if (ok) {
        UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    } else {
        bool undone = local_undo();
        Q_ASSERT(undone);
    }
934
935
936
    return ok;
}

937
938
939
940
941
942
943
944
945
946
947
948
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;
    }
}
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966

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;
        }
    }

Nicolas Carion's avatar
Nicolas Carion committed
967
    int selectionCount = 0;
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
    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;
            }
Nicolas Carion's avatar
Nicolas Carion committed
991
992
993
            if (getType(elem.first) == GroupType::Selection) {
                selectionCount++;
            }
994
995
996
997
998
999
1000
            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;