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 ¤tCategories, 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 }