File indexing completed on 2024-04-28 04:51:20

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "markerlistmodel.hpp"
0007 #include "bin/projectclip.h"
0008 #include "bin/projectitemmodel.h"
0009 #include "core.h"
0010 #include "dialogs/exportguidesdialog.h"
0011 #include "dialogs/markerdialog.h"
0012 #include "doc/docundostack.hpp"
0013 #include "doc/kdenlivedoc.h"
0014 #include "kdenlivesettings.h"
0015 #include "macros.hpp"
0016 #include "project/projectmanager.h"
0017 #include "timeline2/model/snapmodel.hpp"
0018 
0019 #include <QDebug>
0020 #include <QJsonArray>
0021 #include <QJsonDocument>
0022 #include <QJsonObject>
0023 #include <utility>
0024 
0025 MarkerListModel::MarkerListModel(QString clipId, std::weak_ptr<DocUndoStack> undo_stack, QObject *parent)
0026     : QAbstractListModel(parent)
0027     , m_undoStack(std::move(undo_stack))
0028     , m_clipId(std::move(clipId))
0029     , m_lock(QReadWriteLock::Recursive)
0030 {
0031     setup();
0032 }
0033 
0034 void MarkerListModel::setup()
0035 {
0036     // We connect the signals of the abstractitemmodel to a more generic one.
0037     connect(this, &MarkerListModel::columnsMoved, this, &MarkerListModel::modelChanged);
0038     connect(this, &MarkerListModel::columnsRemoved, this, &MarkerListModel::modelChanged);
0039     connect(this, &MarkerListModel::columnsInserted, this, &MarkerListModel::modelChanged);
0040     connect(this, &MarkerListModel::rowsMoved, this, &MarkerListModel::modelChanged);
0041     connect(this, &MarkerListModel::rowsRemoved, this, &MarkerListModel::modelChanged);
0042     connect(this, &MarkerListModel::rowsInserted, this, &MarkerListModel::modelChanged);
0043     connect(this, &MarkerListModel::modelReset, this, &MarkerListModel::modelChanged);
0044     connect(this, &MarkerListModel::dataChanged, this, &MarkerListModel::modelChanged);
0045 }
0046 
0047 void MarkerListModel::loadCategoriesWithUndo(const QStringList &categories, const QStringList &currentCategories, const QMap<int, int> remapCategories)
0048 {
0049     // Remove all markers of deleted category
0050     Fun local_undo = []() { return true; };
0051     Fun local_redo = []() { return true; };
0052     QList<int> deletedCategories = loadCategories(categories);
0053     while (!deletedCategories.isEmpty()) {
0054         int ix = deletedCategories.takeFirst();
0055         QList<CommentedTime> toDelete = getAllMarkers(ix);
0056         if (remapCategories.contains(ix)) {
0057             int newType = remapCategories.value(ix);
0058             for (CommentedTime c : toDelete) {
0059                 addMarker(c.time(), c.comment(), newType, local_undo, local_redo);
0060             }
0061         } else {
0062             for (CommentedTime c : toDelete) {
0063                 removeMarker(c.time(), local_undo, local_redo);
0064             }
0065         }
0066     }
0067     Fun undo = [this, currentCategories]() {
0068         loadCategories(currentCategories);
0069         return true;
0070     };
0071     Fun redo = [this, categories]() {
0072         loadCategories(categories);
0073         return true;
0074     };
0075     PUSH_FRONT_LAMBDA(local_redo, redo);
0076     PUSH_LAMBDA(local_undo, undo);
0077     pCore->pushUndo(undo, redo, i18n("Update guides categories"));
0078 }
0079 
0080 QList<int> MarkerListModel::loadCategories(const QStringList &categories, bool notify)
0081 {
0082     QList<int> previousCategories = pCore->markerTypes.keys();
0083     pCore->markerTypes.clear();
0084     for (const QString &cat : categories) {
0085         if (cat.count(QLatin1Char(':')) < 2) {
0086             // Invalid guide data found
0087             qDebug() << "Invalid guide data found: " << cat;
0088             continue;
0089         }
0090         const QColor color(cat.section(QLatin1Char(':'), -1).simplified());
0091         const QString name = cat.section(QLatin1Char(':'), 0, -3);
0092         int ix = cat.section(QLatin1Char(':'), -2, -2).toInt();
0093         previousCategories.removeAll(ix);
0094         pCore->markerTypes.insert(ix, {color, name});
0095     }
0096     Q_EMIT categoriesChanged();
0097     // Trigger a refresh of all markers
0098     if (notify) {
0099         Q_EMIT dataChanged(index(0), index(m_markerList.size() - 1), {ColorRole});
0100     }
0101     return previousCategories;
0102 }
0103 
0104 const QStringList MarkerListModel::categoriesToStringList() const
0105 {
0106     QStringList categories;
0107     QMapIterator<int, Core::MarkerCategory> i(pCore->markerTypes);
0108     while (i.hasNext()) {
0109         i.next();
0110         categories << QString("%1:%2:%3").arg(i.value().displayName, QString::number(i.key()), i.value().color.name());
0111     }
0112     return categories;
0113 }
0114 
0115 QStringList MarkerListModel::guideCategoriesToStringList(const QString &categoriesData)
0116 {
0117     QStringList categories;
0118     if (categoriesData.isEmpty()) {
0119         return categories;
0120     }
0121     auto json = QJsonDocument::fromJson(categoriesData.toUtf8());
0122     if (!json.isArray()) {
0123         qDebug() << "Error : Json file should be an array";
0124         return categories;
0125     }
0126     auto list = json.array();
0127     for (const auto &entry : qAsConst(list)) {
0128         if (!entry.isObject()) {
0129             qDebug() << "Warning : Skipping invalid category data";
0130             continue;
0131         }
0132         auto entryObj = entry.toObject();
0133         if (!entryObj.contains(QLatin1String("index"))) {
0134             qDebug() << "Warning : Skipping invalid category data (does not contain index)";
0135             continue;
0136         }
0137         int ix = entryObj[QLatin1String("index")].toInt();
0138         QString comment = entryObj[QLatin1String("comment")].toString();
0139         QString color = entryObj[QLatin1String("color")].toString();
0140         categories << QString("%1:%2:%3").arg(comment, QString::number(ix), color);
0141     }
0142     return categories;
0143 }
0144 
0145 const QString MarkerListModel::categoriesListToJSon(const QStringList categories)
0146 {
0147     QJsonArray list;
0148     for (auto &cat : categories) {
0149         QJsonObject currentMarker;
0150         const QColor color(cat.section(QLatin1Char(':'), -1).simplified());
0151         const QString name = cat.section(QLatin1Char(':'), 0, -3);
0152         int ix = cat.section(QLatin1Char(':'), -2, -2).toInt();
0153         currentMarker.insert(QLatin1String("index"), QJsonValue(ix));
0154         currentMarker.insert(QLatin1String("comment"), QJsonValue(name));
0155         currentMarker.insert(QLatin1String("color"), QJsonValue(color.name()));
0156         list.push_back(currentMarker);
0157     }
0158     QJsonDocument json(list);
0159     return QString::fromUtf8(json.toJson());
0160 }
0161 
0162 const QString MarkerListModel::categoriesToJSon() const
0163 {
0164     QJsonArray list;
0165     QMapIterator<int, Core::MarkerCategory> i(pCore->markerTypes);
0166     while (i.hasNext()) {
0167         i.next();
0168         QJsonObject currentMarker;
0169         currentMarker.insert(QLatin1String("index"), QJsonValue(i.key()));
0170         currentMarker.insert(QLatin1String("comment"), QJsonValue(i.value().displayName));
0171         currentMarker.insert(QLatin1String("color"), QJsonValue(i.value().color.name()));
0172         list.push_back(currentMarker);
0173     }
0174     QJsonDocument json(list);
0175     return QString::fromUtf8(json.toJson());
0176 }
0177 
0178 int MarkerListModel::markerIdAtFrame(int pos) const
0179 {
0180     if (m_markerPositions.contains(pos)) {
0181         return m_markerPositions.value(pos);
0182     }
0183     return -1;
0184 }
0185 
0186 bool MarkerListModel::hasMarker(GenTime pos) const
0187 {
0188     int frame = pos.frames(pCore->getCurrentFps());
0189     return hasMarker(frame);
0190 }
0191 
0192 CommentedTime MarkerListModel::markerById(int mid) const
0193 {
0194     Q_ASSERT(m_markerPositions.values().contains(mid));
0195     return m_markerList.at(mid);
0196 }
0197 
0198 CommentedTime MarkerListModel::marker(int frame) const
0199 {
0200     int mid = markerIdAtFrame(frame);
0201     if (mid > -1) {
0202         return m_markerList.at(mid);
0203     }
0204     return CommentedTime();
0205 }
0206 
0207 CommentedTime MarkerListModel::marker(GenTime pos) const
0208 {
0209     int mid = markerIdAtFrame(pos.frames(pCore->getCurrentFps()));
0210     if (mid > -1) {
0211         return m_markerList.at(mid);
0212     }
0213     return CommentedTime();
0214 }
0215 
0216 bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo)
0217 {
0218     QWriteLocker locker(&m_lock);
0219     Fun local_undo = []() { return true; };
0220     Fun local_redo = []() { return true; };
0221     if (type == -1) type = KdenliveSettings::default_marker_type();
0222     Q_ASSERT(pCore->markerTypes.contains(type));
0223     if (hasMarker(pos)) {
0224         // In this case we simply change the comment and type
0225         CommentedTime current = marker(pos);
0226         local_undo = changeComment_lambda(pos, current.comment(), current.markerType());
0227         local_redo = changeComment_lambda(pos, comment, type);
0228     } else {
0229         // In this case we create one
0230         local_redo = addMarker_lambda(pos, comment, type);
0231         local_undo = deleteMarker_lambda(pos);
0232     }
0233     if (local_redo()) {
0234         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0235         return true;
0236     }
0237     return false;
0238 }
0239 
0240 bool MarkerListModel::addMarkers(const QMap<GenTime, QString> &markers, int type)
0241 {
0242     QWriteLocker locker(&m_lock);
0243     Fun undo = []() { return true; };
0244     Fun redo = []() { return true; };
0245 
0246     QMapIterator<GenTime, QString> i(markers);
0247     bool rename = false;
0248     bool res = true;
0249     while (i.hasNext() && res) {
0250         i.next();
0251         if (hasMarker(i.key())) {
0252             rename = true;
0253         }
0254         res = addMarker(i.key(), i.value(), type, undo, redo);
0255     }
0256     if (res) {
0257         if (rename) {
0258             PUSH_UNDO(undo, redo, i18n("Rename marker"));
0259         } else {
0260             PUSH_UNDO(undo, redo, i18n("Add marker"));
0261         }
0262     }
0263     return res;
0264 }
0265 
0266 bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type)
0267 {
0268     QWriteLocker locker(&m_lock);
0269     Fun undo = []() { return true; };
0270     Fun redo = []() { return true; };
0271 
0272     bool rename = hasMarker(pos);
0273     bool res = addMarker(pos, comment, type, undo, redo);
0274     if (res) {
0275         if (rename) {
0276             PUSH_UNDO(undo, redo, i18n("Rename marker"));
0277         } else {
0278             PUSH_UNDO(undo, redo, i18n("Add marker"));
0279         }
0280     }
0281     return res;
0282 }
0283 
0284 bool MarkerListModel::removeMarker(GenTime pos, Fun &undo, Fun &redo)
0285 {
0286     QWriteLocker locker(&m_lock);
0287     if (hasMarker(pos) == false) {
0288         return false;
0289     }
0290     CommentedTime current = marker(pos);
0291     Fun local_undo = addMarker_lambda(pos, current.comment(), current.markerType());
0292     Fun local_redo = deleteMarker_lambda(pos);
0293     if (local_redo()) {
0294         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0295         return true;
0296     }
0297     return false;
0298 }
0299 
0300 bool MarkerListModel::removeMarker(GenTime pos)
0301 {
0302     QWriteLocker locker(&m_lock);
0303     Fun undo = []() { return true; };
0304     Fun redo = []() { return true; };
0305 
0306     bool res = removeMarker(pos, undo, redo);
0307     if (res) {
0308         PUSH_UNDO(undo, redo, i18n("Delete marker"));
0309     }
0310     return res;
0311 }
0312 
0313 bool MarkerListModel::editMarker(GenTime oldPos, GenTime pos, QString comment, int type)
0314 {
0315     QWriteLocker locker(&m_lock);
0316     Q_ASSERT(hasMarker(oldPos));
0317     CommentedTime current = marker(oldPos);
0318     if (comment.isEmpty()) {
0319         comment = current.comment();
0320     }
0321     if (type == -1) {
0322         type = current.markerType();
0323     }
0324     if (oldPos == pos && current.comment() == comment && current.markerType() == type) return true;
0325     Fun undo = []() { return true; };
0326     Fun redo = []() { return true; };
0327     bool res = true;
0328     if (oldPos != pos) {
0329         res = removeMarker(oldPos, undo, redo);
0330     }
0331     if (res) {
0332         res = addMarker(pos, comment, type, undo, redo);
0333     }
0334     if (res) {
0335         PUSH_UNDO(undo, redo, i18n("Edit marker"));
0336     } else {
0337         bool undone = undo();
0338         Q_ASSERT(undone);
0339     }
0340     return res;
0341 }
0342 
0343 int MarkerListModel::getRowfromId(int mid) const
0344 {
0345     READ_LOCK();
0346     Q_ASSERT(m_markerList.count(mid) > 0);
0347     return (int)std::distance(m_markerList.begin(), m_markerList.find(mid));
0348 }
0349 
0350 int MarkerListModel::getIdFromPos(const GenTime &pos) const
0351 {
0352     int frame = pos.frames(pCore->getCurrentFps());
0353     return getIdFromPos(frame);
0354 }
0355 
0356 int MarkerListModel::getIdFromPos(int frame) const
0357 {
0358     if (m_markerPositions.contains(frame)) {
0359         return m_markerPositions.value(frame);
0360     }
0361     return -1;
0362 }
0363 
0364 bool MarkerListModel::moveMarker(int mid, GenTime pos)
0365 {
0366     QWriteLocker locker(&m_lock);
0367     Q_ASSERT(m_markerList.count(mid) > 0);
0368     if (hasMarker(pos)) {
0369         // A marker / guide already exists at new position, abort
0370         return false;
0371     }
0372     int row = getRowfromId(mid);
0373     int oldPos = m_markerList.at(mid).time().frames(pCore->getCurrentFps());
0374     m_markerList[mid].setTime(pos);
0375     m_markerPositions.remove(oldPos);
0376     m_markerPositions.insert(pos.frames(pCore->getCurrentFps()), mid);
0377     Q_EMIT dataChanged(index(row), index(row), {FrameRole});
0378     return true;
0379 }
0380 
0381 void MarkerListModel::moveMarkersWithoutUndo(const QVector<int> &markersId, int offset, bool updateView)
0382 {
0383     QWriteLocker locker(&m_lock);
0384     if (markersId.length() <= 0) {
0385         return;
0386     }
0387     int firstRow = -1;
0388     int lastRow = -1;
0389     for (auto mid : markersId) {
0390         Q_ASSERT(m_markerList.count(mid) > 0);
0391         GenTime t = m_markerList.at(mid).time();
0392         m_markerPositions.remove(t.frames(pCore->getCurrentFps()));
0393         t += GenTime(offset, pCore->getCurrentFps());
0394         m_markerPositions.insert(t.frames(pCore->getCurrentFps()), mid);
0395         m_markerList[mid].setTime(t);
0396         if (!updateView) {
0397             continue;
0398         }
0399         if (firstRow == -1) {
0400             firstRow = getRowfromId(mid);
0401             lastRow = firstRow;
0402         } else {
0403             int row = getRowfromId(mid);
0404             if (row > lastRow) {
0405                 lastRow = row;
0406             } else if (row < firstRow) {
0407                 firstRow = row;
0408             }
0409         }
0410     }
0411     if (updateView) {
0412         Q_EMIT dataChanged(index(firstRow), index(lastRow), {FrameRole});
0413     }
0414 }
0415 
0416 bool MarkerListModel::moveMarkers(const QList<CommentedTime> &markers, GenTime fromPos, GenTime toPos, Fun &undo, Fun &redo)
0417 {
0418     QWriteLocker locker(&m_lock);
0419 
0420     if (markers.length() <= 0) {
0421         return false;
0422     }
0423 
0424     bool res = false;
0425     for (const auto &marker : markers) {
0426 
0427         GenTime oldPos = marker.time();
0428         QString oldComment = marker.comment();
0429         int oldType = marker.markerType();
0430         GenTime newPos = oldPos.operator+(toPos.operator-(fromPos));
0431 
0432         res = removeMarker(oldPos, undo, redo);
0433         if (res) {
0434             res = addMarker(newPos, oldComment, oldType, undo, redo);
0435         } else {
0436             break;
0437         }
0438     }
0439     return res;
0440 }
0441 
0442 Fun MarkerListModel::changeComment_lambda(GenTime pos, const QString &comment, int type)
0443 {
0444     QWriteLocker locker(&m_lock);
0445     return [pos, comment, type, this]() {
0446         Q_ASSERT(hasMarker(pos));
0447         int mid = getIdFromPos(pos);
0448         int row = getRowfromId(mid);
0449         m_markerList[mid].setComment(comment);
0450         m_markerList[mid].setMarkerType(type);
0451         Q_EMIT dataChanged(index(row), index(row), {CommentRole, ColorRole});
0452         return true;
0453     };
0454 }
0455 
0456 Fun MarkerListModel::addMarker_lambda(GenTime pos, const QString &comment, int type)
0457 {
0458     QWriteLocker locker(&m_lock);
0459     return [pos, comment, type, this]() {
0460         Q_ASSERT(hasMarker(pos) == false);
0461         // We determine the row of the newly added marker
0462         int mid = TimelineModel::getNextId();
0463         int insertionRow = static_cast<int>(m_markerList.size());
0464         beginInsertRows(QModelIndex(), insertionRow, insertionRow);
0465         m_markerList[mid] = CommentedTime(pos, comment, type);
0466         m_markerPositions.insert(pos.frames(pCore->getCurrentFps()), mid);
0467         endInsertRows();
0468         addSnapPoint(pos);
0469         return true;
0470     };
0471 }
0472 
0473 Fun MarkerListModel::deleteMarker_lambda(GenTime pos)
0474 {
0475     QWriteLocker locker(&m_lock);
0476     return [pos, this]() {
0477         Q_ASSERT(hasMarker(pos));
0478         int mid = getIdFromPos(pos);
0479         int row = getRowfromId(mid);
0480         beginRemoveRows(QModelIndex(), row, row);
0481         m_markerList.erase(mid);
0482         m_markerPositions.remove(pos.frames(pCore->getCurrentFps()));
0483         endRemoveRows();
0484         removeSnapPoint(pos);
0485         return true;
0486     };
0487 }
0488 
0489 std::shared_ptr<MarkerListModel> MarkerListModel::getModel(const QString &clipId)
0490 {
0491     return pCore->projectItemModel()->getClipByBinID(clipId)->getMarkerModel();
0492 }
0493 
0494 QHash<int, QByteArray> MarkerListModel::roleNames() const
0495 {
0496     QHash<int, QByteArray> roles;
0497     roles[CommentRole] = "comment";
0498     roles[PosRole] = "position";
0499     roles[FrameRole] = "frame";
0500     roles[ColorRole] = "color";
0501     roles[TypeRole] = "type";
0502     roles[IdRole] = "id";
0503     return roles;
0504 }
0505 
0506 void MarkerListModel::addSnapPoint(GenTime pos)
0507 {
0508     QWriteLocker locker(&m_lock);
0509     std::vector<std::weak_ptr<SnapInterface>> validSnapModels;
0510     for (const auto &snapModel : m_registeredSnaps) {
0511         if (auto ptr = snapModel.lock()) {
0512             validSnapModels.push_back(snapModel);
0513             ptr->addPoint(pos.frames(pCore->getCurrentFps()));
0514         }
0515     }
0516     // Update the list of snapModel known to be valid
0517     std::swap(m_registeredSnaps, validSnapModels);
0518 }
0519 
0520 void MarkerListModel::removeSnapPoint(GenTime pos)
0521 {
0522     QWriteLocker locker(&m_lock);
0523     std::vector<std::weak_ptr<SnapInterface>> validSnapModels;
0524     for (const auto &snapModel : m_registeredSnaps) {
0525         if (auto ptr = snapModel.lock()) {
0526             validSnapModels.push_back(snapModel);
0527             ptr->removePoint(pos.frames(pCore->getCurrentFps()));
0528         }
0529     }
0530     // Update the list of snapModel known to be valid
0531     std::swap(m_registeredSnaps, validSnapModels);
0532 }
0533 
0534 QVariant MarkerListModel::data(const QModelIndex &index, int role) const
0535 {
0536     READ_LOCK();
0537     if (index.row() < 0 || index.row() >= static_cast<int>(m_markerList.size()) || !index.isValid()) {
0538         return QVariant();
0539     }
0540     auto it = m_markerList.begin();
0541     std::advance(it, index.row());
0542     switch (role) {
0543     case Qt::DisplayRole:
0544     case Qt::EditRole:
0545     case CommentRole:
0546         return it->second.comment();
0547     case PosRole:
0548         return it->second.time().seconds();
0549     case FrameRole:
0550     case Qt::UserRole:
0551         return it->second.time().frames(pCore->getCurrentFps());
0552     case ColorRole:
0553     case Qt::DecorationRole:
0554         return pCore->markerTypes.value(it->second.markerType()).color;
0555     case TypeRole:
0556         return it->second.markerType();
0557     case IdRole:
0558         return it->first;
0559     case TCRole:
0560         return pCore->timecode().getDisplayTimecode(it->second.time(), false);
0561     }
0562     return QVariant();
0563 }
0564 
0565 int MarkerListModel::rowCount(const QModelIndex &parent) const
0566 {
0567     READ_LOCK();
0568     if (parent.isValid()) return 0;
0569     return static_cast<int>(m_markerList.size());
0570 }
0571 
0572 CommentedTime MarkerListModel::getMarker(int frame, bool *ok) const
0573 {
0574     READ_LOCK();
0575     if (hasMarker(frame) == false) {
0576         // return empty marker
0577         *ok = false;
0578         return CommentedTime();
0579     }
0580     *ok = true;
0581     return marker(frame);
0582 }
0583 
0584 CommentedTime MarkerListModel::getMarker(const GenTime &pos, bool *ok) const
0585 {
0586     READ_LOCK();
0587     if (hasMarker(pos) == false) {
0588         // return empty marker
0589         *ok = false;
0590         return CommentedTime();
0591     }
0592     *ok = true;
0593     return marker(pos);
0594 }
0595 
0596 QList<CommentedTime> MarkerListModel::getAllMarkers(int type) const
0597 {
0598     READ_LOCK();
0599     QList<CommentedTime> markers;
0600     for (const auto &marker : m_markerList) {
0601         if (type == -1 || marker.second.markerType() == type) {
0602             markers << marker.second;
0603         }
0604     }
0605     std::sort(markers.begin(), markers.end());
0606     return markers;
0607 }
0608 
0609 QList<CommentedTime> MarkerListModel::getMarkersInRange(int start, int end) const
0610 {
0611     QList<CommentedTime> markers;
0612     QVector<int> mids = getMarkersIdInRange(start, end);
0613     // Now extract markers
0614     READ_LOCK();
0615     for (const auto &marker : mids) {
0616         markers << m_markerList.at(marker);
0617     }
0618     std::sort(markers.begin(), markers.end());
0619     return markers;
0620 }
0621 
0622 int MarkerListModel::getMarkerPos(int mid) const
0623 {
0624     READ_LOCK();
0625     Q_ASSERT(m_markerList.count(mid) > 0);
0626     return m_markerPositions.key(mid);
0627 }
0628 
0629 QVector<int> MarkerListModel::getMarkersIdInRange(int start, int end) const
0630 {
0631     READ_LOCK();
0632     // First find marker ids in range
0633     QVector<int> markers;
0634     QMap<int, int>::const_iterator i = m_markerPositions.constBegin();
0635     while (i != m_markerPositions.constEnd()) {
0636         if (end > -1 && i.key() > end) {
0637             break;
0638         }
0639         if (i.key() >= start) {
0640             markers << i.value();
0641         }
0642         ++i;
0643     }
0644     return markers;
0645 }
0646 
0647 std::vector<int> MarkerListModel::getSnapPoints() const
0648 {
0649     READ_LOCK();
0650     const QList<int> positions = m_markerPositions.keys();
0651     std::vector<int> markers(positions.cbegin(), positions.cend());
0652     return markers;
0653 }
0654 
0655 bool MarkerListModel::hasMarker(int frame) const
0656 {
0657     READ_LOCK();
0658     return m_markerPositions.contains(frame);
0659 }
0660 
0661 void MarkerListModel::registerSnapModel(const std::weak_ptr<SnapInterface> &snapModel)
0662 {
0663     READ_LOCK();
0664     // make sure ptr is valid
0665     if (auto ptr = snapModel.lock()) {
0666         // ptr is valid, we store it
0667         m_registeredSnaps.push_back(snapModel);
0668 
0669         // we now add the already existing markers to the snap
0670         QMap<int, int>::const_iterator i = m_markerPositions.constBegin();
0671         while (i != m_markerPositions.constEnd()) {
0672             ptr->addPoint(i.key());
0673             ++i;
0674         }
0675     } else {
0676         qDebug() << "Error: added snapmodel is null";
0677         Q_ASSERT(false);
0678     }
0679 }
0680 
0681 bool MarkerListModel::importFromFile(const QString &fileData, bool ignoreConflicts)
0682 {
0683     Fun undo = []() { return true; };
0684     Fun redo = []() { return true; };
0685     bool res = importFromJson(fileData, ignoreConflicts, undo, redo);
0686     if (!res) {
0687         res = importFromTxt(fileData, undo, redo);
0688     }
0689     if (res) {
0690         PUSH_UNDO(undo, redo, i18n("Import markers"));
0691     }
0692     return res;
0693 }
0694 
0695 bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo)
0696 {
0697     Fun undo = []() { return true; };
0698     Fun redo = []() { return true; };
0699     bool result = importFromJson(data, ignoreConflicts, undo, redo);
0700     if (result && pushUndo) {
0701         PUSH_UNDO(undo, redo, i18n("Import markers"));
0702     }
0703     return result;
0704 }
0705 
0706 bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo)
0707 {
0708     if (data.isEmpty()) {
0709         return false;
0710     }
0711     QWriteLocker locker(&m_lock);
0712     auto json = QJsonDocument::fromJson(data.toUtf8());
0713     if (!json.isArray()) {
0714         qDebug() << "Error : Json file should be an array";
0715         return false;
0716     }
0717     auto list = json.array();
0718     for (const auto &entry : qAsConst(list)) {
0719         if (!entry.isObject()) {
0720             qDebug() << "Warning : Skipping invalid marker data";
0721             continue;
0722         }
0723         auto entryObj = entry.toObject();
0724         if (!entryObj.contains(QLatin1String("pos"))) {
0725             qDebug() << "Warning : Skipping invalid marker data (does not contain position)";
0726             continue;
0727         }
0728         int pos = entryObj[QLatin1String("pos")].toInt();
0729         QString comment = entryObj[QLatin1String("comment")].toString(i18n("Marker"));
0730         int type = entryObj[QLatin1String("type")].toInt(0);
0731         if (!pCore->markerTypes.contains(type)) {
0732             qDebug() << "Warning : invalid type found:" << type << " Recovering category";
0733             type = type % 9;
0734             if (!pCore->markerTypes.contains(type)) {
0735                 QString originalCategory = KdenliveDoc::getDefaultGuideCategories().at(type);
0736                 QColor color(originalCategory.section(QLatin1Char(':'), -1));
0737                 pCore->markerTypes.insert(type, {color, i18n("Recovered %1", type)});
0738                 Q_EMIT categoriesChanged();
0739                 Q_EMIT pCore->updateDefaultMarkerCategory();
0740             }
0741         }
0742         bool res = true;
0743         if (!ignoreConflicts && hasMarker(GenTime(pos, pCore->getCurrentFps()))) {
0744             // potential conflict found, checking
0745             CommentedTime oldMarker = marker(GenTime(pos, pCore->getCurrentFps()));
0746             res = (oldMarker.comment() == comment) && (type == oldMarker.markerType());
0747         }
0748         qDebug() << "// ADDING MARKER AT POS: " << pos << ", FPS: " << pCore->getCurrentFps();
0749         res = res && addMarker(GenTime(pos, pCore->getCurrentFps()), comment, type, undo, redo);
0750         if (!res) {
0751             bool undone = undo();
0752             Q_ASSERT(undone);
0753             return false;
0754         }
0755     }
0756     return true;
0757 }
0758 
0759 bool MarkerListModel::importFromTxt(const QString &fileData, Fun &undo, Fun &redo)
0760 {
0761     QWriteLocker locker(&m_lock);
0762     bool res = false;
0763     bool lineRead = false;
0764     int type = KdenliveSettings::default_marker_type();
0765     const QStringList lines = fileData.split(QLatin1Char('\n'));
0766     for (auto &line : lines) {
0767         if (line.isEmpty()) {
0768             continue;
0769         }
0770         QString pos = line.section(QLatin1Char(' '), 0, 0);
0771         GenTime position;
0772         // Try to read timecode
0773         bool ok = false;
0774         int separatorsCount = pos.count(QLatin1Char(':'));
0775         switch (separatorsCount) {
0776         case 0:
0777             // assume we are dealing with seconds
0778             position = GenTime(pos.toDouble(&ok));
0779             break;
0780         case 1: {
0781             // assume min:sec
0782             QString sec = pos.section(QLatin1Char(':'), 1);
0783             QString min = pos.section(QLatin1Char(':'), 0, 0);
0784             double seconds = sec.toDouble(&ok);
0785             int minutes = ok ? min.toInt(&ok) : 0;
0786             position = GenTime(seconds + 60 * minutes);
0787             break;
0788         }
0789         case 2:
0790         default: {
0791             // assume hh:min:sec
0792             QString sec = pos.section(QLatin1Char(':'), 2, 2);
0793             QString min = pos.section(QLatin1Char(':'), 1, 1);
0794             QString hours = pos.section(QLatin1Char(':'), 0, 0);
0795             double seconds = sec.toDouble(&ok);
0796             int minutes = ok ? min.toInt(&ok) : 0;
0797             int h = ok ? hours.toInt(&ok) : 0;
0798             position = GenTime(seconds + (60 * minutes) + (3600 * h));
0799             break;
0800         }
0801         }
0802         if (!ok) {
0803             // Could not read timecode
0804             qDebug() << "::: Could not read timecode from line: " << line;
0805             continue;
0806         }
0807         QString comment = line.section(QLatin1Char(' '), 1);
0808         res = addMarker(position, comment, type, undo, redo);
0809         if (!res) {
0810             break;
0811         } else if (!lineRead) {
0812             lineRead = true;
0813         }
0814     }
0815     return res && lineRead;
0816 }
0817 
0818 QString MarkerListModel::toJson(QList<int> categories) const
0819 {
0820     READ_LOCK();
0821     QJsonArray list;
0822     bool exportAllCategories = categories.isEmpty() || categories == (QList<int>() << -1);
0823     QList<CommentedTime> markers;
0824     for (const auto &marker : m_markerList) {
0825         if (exportAllCategories || categories.contains(marker.second.markerType())) {
0826             markers << marker.second;
0827         }
0828     }
0829     std::sort(markers.begin(), markers.end());
0830     for (const auto &marker : markers) {
0831         QJsonObject currentMarker;
0832         currentMarker.insert(QLatin1String("pos"), QJsonValue(marker.time().frames(pCore->getCurrentFps())));
0833         currentMarker.insert(QLatin1String("comment"), QJsonValue(marker.comment()));
0834         currentMarker.insert(QLatin1String("type"), QJsonValue(marker.markerType()));
0835         list.push_back(currentMarker);
0836     }
0837     QJsonDocument json(list);
0838     return QString::fromUtf8(json.toJson());
0839 }
0840 
0841 bool MarkerListModel::removeAllMarkers()
0842 {
0843     QWriteLocker locker(&m_lock);
0844     std::vector<GenTime> all_pos;
0845     Fun local_undo = []() { return true; };
0846     Fun local_redo = []() { return true; };
0847     for (const auto &m : m_markerList) {
0848         all_pos.push_back(m.second.time());
0849     }
0850     bool res = true;
0851     for (const auto &p : all_pos) {
0852         res = removeMarker(p, local_undo, local_redo);
0853         if (!res) {
0854             bool undone = local_undo();
0855             Q_ASSERT(undone);
0856             return false;
0857         }
0858     }
0859     PUSH_UNDO(local_undo, local_redo, i18n("Delete all markers"));
0860     return true;
0861 }
0862 
0863 bool MarkerListModel::editMultipleMarkersGui(const QList<GenTime> positions, QWidget *parent)
0864 {
0865     bool exists;
0866     auto marker = getMarker(positions.first(), &exists);
0867     if (!exists) {
0868         pCore->displayMessage(i18n("No guide found at current position"), InformationMessage);
0869     }
0870     QDialog d(parent);
0871     d.setWindowTitle(i18n("Edit Markers Category"));
0872     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
0873     auto *l = new QVBoxLayout;
0874     d.setLayout(l);
0875     d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject);
0876     d.connect(buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept);
0877     QLabel lab(i18n("Markers Category"), &d);
0878     MarkerCategoryChooser chooser(&d);
0879     chooser.setMarkerModel(this);
0880     chooser.setAllowAll(false);
0881     chooser.setCurrentCategory(marker.markerType());
0882     l->addWidget(&lab);
0883     l->addWidget(&chooser);
0884     l->addWidget(buttonBox);
0885     if (d.exec() == QDialog::Accepted) {
0886         int category = chooser.currentCategory();
0887         Fun undo = []() { return true; };
0888         Fun redo = []() { return true; };
0889         for (auto &pos : positions) {
0890             marker = getMarker(pos, &exists);
0891             if (exists) {
0892                 addMarker(pos, marker.comment(), category, undo, redo);
0893             }
0894         }
0895         PUSH_UNDO(undo, redo, i18n("Edit markers"));
0896         return true;
0897     }
0898     return false;
0899 }
0900 
0901 bool MarkerListModel::editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ProjectClip *clip, bool createOnly)
0902 {
0903     bool exists;
0904     auto marker = getMarker(pos, &exists);
0905     if (!exists && !createIfNotFound) {
0906         pCore->displayMessage(i18n("No guide found at current position"), InformationMessage);
0907     }
0908 
0909     if (!exists && createIfNotFound) {
0910         marker = CommentedTime(pos, QString(), KdenliveSettings::default_marker_type());
0911     }
0912 
0913     QScopedPointer<MarkerDialog> dialog(new MarkerDialog(clip, marker, i18n("Edit Marker"), false, parent));
0914 
0915     if (dialog->exec() == QDialog::Accepted) {
0916         marker = dialog->newMarker();
0917         Q_EMIT pCore->updateDefaultMarkerCategory();
0918         if (exists && !createOnly) {
0919             return editMarker(pos, marker.time(), marker.comment(), marker.markerType());
0920         }
0921         return addMarker(marker.time(), marker.comment(), marker.markerType());
0922     }
0923     return false;
0924 }
0925 
0926 bool MarkerListModel::addMultipleMarkersGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ProjectClip *clip)
0927 {
0928     bool exists;
0929     auto marker = getMarker(pos, &exists);
0930     if (!exists && !createIfNotFound) {
0931         pCore->displayMessage(i18n("No guide found at current position"), InformationMessage);
0932     }
0933 
0934     if (!exists && createIfNotFound) {
0935         marker = CommentedTime(pos, QString(), KdenliveSettings::default_marker_type());
0936     }
0937 
0938     QScopedPointer<MarkerDialog> dialog(new MarkerDialog(clip, marker, i18n("Add Markers"), true, parent));
0939     if (dialog->exec() == QDialog::Accepted) {
0940         int max = dialog->addMultiMarker() ? dialog->getOccurrences() : 1;
0941         GenTime interval = dialog->getInterval();
0942         KdenliveSettings::setMultipleguidesinterval(interval.seconds());
0943         marker = dialog->newMarker();
0944         Q_EMIT pCore->updateDefaultMarkerCategory();
0945         GenTime startTime = marker.time();
0946         QWriteLocker locker(&m_lock);
0947         Fun undo = []() { return true; };
0948         Fun redo = []() { return true; };
0949         for (int i = 0; i < max; i++) {
0950             addMarker(startTime, marker.comment(), marker.markerType(), undo, redo);
0951             startTime += interval;
0952         }
0953         PUSH_UNDO(undo, redo, i18n("Add markers"));
0954     }
0955     return false;
0956 }
0957 
0958 void MarkerListModel::exportGuidesGui(QWidget *parent, GenTime projectDuration) const
0959 {
0960     QScopedPointer<ExportGuidesDialog> dialog(new ExportGuidesDialog(this, projectDuration, parent));
0961     dialog->exec();
0962 }