diff --git a/src/bin/model/markerlistmodel.cpp b/src/bin/model/markerlistmodel.cpp index 29d25c9c4..4b1725d6c 100644 --- a/src/bin/model/markerlistmodel.cpp +++ b/src/bin/model/markerlistmodel.cpp @@ -1,481 +1,483 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #include "markerlistmodel.hpp" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/markerdialog.h" #include "doc/docundostack.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "project/projectmanager.h" #include "timeline2/model/snapmodel.hpp" #include #include #include #include #include std::array MarkerListModel::markerTypes{{Qt::red, Qt::blue, Qt::green, Qt::yellow, Qt::cyan}}; MarkerListModel::MarkerListModel(QString clipId, std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_undoStack(std::move(undo_stack)) , m_guide(false) , m_clipId(std::move(clipId)) , m_lock(QReadWriteLock::Recursive) { setup(); } MarkerListModel::MarkerListModel(std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_undoStack(std::move(undo_stack)) , m_guide(true) , m_lock(QReadWriteLock::Recursive) { setup(); } void MarkerListModel::setup() { // We connect the signals of the abstractitemmodel to a more generic one. connect(this, &MarkerListModel::columnsMoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::columnsRemoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::columnsInserted, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsMoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsRemoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsInserted, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::modelReset, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::dataChanged, this, &MarkerListModel::modelChanged); } bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; if (type == -1) type = KdenliveSettings::default_marker_type(); Q_ASSERT(type >= 0 && type < (int)markerTypes.size()); if (m_markerList.count(pos) > 0) { // In this case we simply change the comment and type QString oldComment = m_markerList[pos].first; int oldType = m_markerList[pos].second; local_undo = changeComment_lambda(pos, oldComment, oldType); local_redo = changeComment_lambda(pos, comment, type); } else { // In this case we create one local_redo = addMarker_lambda(pos, comment, type); local_undo = deleteMarker_lambda(pos); } if (local_redo()) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool rename = (m_markerList.count(pos) > 0); bool res = addMarker(pos, comment, type, undo, redo); if (res) { if (rename) { PUSH_UNDO(undo, redo, m_guide ? i18n("Rename guide") : i18n("Rename marker")); } else { PUSH_UNDO(undo, redo, m_guide ? i18n("Add guide") : i18n("Add marker")); } } return res; } bool MarkerListModel::removeMarker(GenTime pos, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); - Q_ASSERT(m_markerList.count(pos) > 0); + if (m_markerList.count(pos) == 0) { + return false; + } QString oldComment = m_markerList[pos].first; int oldType = m_markerList[pos].second; Fun local_undo = addMarker_lambda(pos, oldComment, oldType); Fun local_redo = deleteMarker_lambda(pos); if (local_redo()) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool MarkerListModel::removeMarker(GenTime pos) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = removeMarker(pos, undo, redo); if (res) { PUSH_UNDO(undo, redo, m_guide ? i18n("Delete guide") : i18n("Delete marker")); } return res; } bool MarkerListModel::editMarker(GenTime oldPos, GenTime pos, QString comment, int type) { QWriteLocker locker(&m_lock); Q_ASSERT(m_markerList.count(oldPos) > 0); QString oldComment = m_markerList[oldPos].first; int oldType = m_markerList[oldPos].second; if (comment.isEmpty()) { comment = oldComment; } if (type == -1) { type = oldType; } if (oldPos == pos && oldComment == comment && oldType == type) return true; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = removeMarker(oldPos, undo, redo); if (res) { res = addMarker(pos, comment, type, undo, redo); } if (res) { PUSH_UNDO(undo, redo, m_guide ? i18n("Edit guide") : i18n("Edit marker")); } else { bool undone = undo(); Q_ASSERT(undone); } return res; } Fun MarkerListModel::changeComment_lambda(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos, comment, type]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) > 0); int row = static_cast(std::distance(model->m_markerList.begin(), model->m_markerList.find(pos))); model->m_markerList[pos].first = comment; model->m_markerList[pos].second = type; emit model->dataChanged(model->index(row), model->index(row), QVector() << CommentRole << ColorRole); return true; }; } Fun MarkerListModel::addMarker_lambda(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos, comment, type]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) == 0); // We determine the row of the newly added marker auto insertionIt = model->m_markerList.lower_bound(pos); int insertionRow = static_cast(model->m_markerList.size()); if (insertionIt != model->m_markerList.end()) { insertionRow = static_cast(std::distance(model->m_markerList.begin(), insertionIt)); } model->beginInsertRows(QModelIndex(), insertionRow, insertionRow); model->m_markerList[pos] = {comment, type}; model->endInsertRows(); model->addSnapPoint(pos); return true; }; } Fun MarkerListModel::deleteMarker_lambda(GenTime pos) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) > 0); int row = static_cast(std::distance(model->m_markerList.begin(), model->m_markerList.find(pos))); model->beginRemoveRows(QModelIndex(), row, row); model->m_markerList.erase(pos); model->endRemoveRows(); model->removeSnapPoint(pos); return true; }; } std::shared_ptr MarkerListModel::getModel(bool guide, const QString &clipId) { if (guide) { return pCore->projectManager()->getGuideModel(); } return pCore->bin()->getBinClip(clipId)->getMarkerModel(); } QHash MarkerListModel::roleNames() const { QHash roles; roles[CommentRole] = "comment"; roles[PosRole] = "position"; roles[FrameRole] = "frame"; roles[ColorRole] = "color"; roles[TypeRole] = "type"; return roles; } void MarkerListModel::addSnapPoint(GenTime pos) { QWriteLocker locker(&m_lock); std::vector> validSnapModels; for (const auto &snapModel : m_registeredSnaps) { if (auto ptr = snapModel.lock()) { validSnapModels.push_back(snapModel); ptr->addPoint(pos.frames(pCore->getCurrentFps())); } } // Update the list of snapModel known to be valid std::swap(m_registeredSnaps, validSnapModels); } void MarkerListModel::removeSnapPoint(GenTime pos) { QWriteLocker locker(&m_lock); std::vector> validSnapModels; for (const auto &snapModel : m_registeredSnaps) { if (auto ptr = snapModel.lock()) { validSnapModels.push_back(snapModel); ptr->removePoint(pos.frames(pCore->getCurrentFps())); } } // Update the list of snapModel known to be valid std::swap(m_registeredSnaps, validSnapModels); } QVariant MarkerListModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (index.row() < 0 || index.row() >= static_cast(m_markerList.size()) || !index.isValid()) { return QVariant(); } auto it = m_markerList.begin(); std::advance(it, index.row()); switch (role) { case Qt::DisplayRole: case Qt::EditRole: case CommentRole: return it->second.first; case PosRole: return it->first.seconds(); case FrameRole: case Qt::UserRole: return it->first.frames(pCore->getCurrentFps()); case ColorRole: case Qt::DecorationRole: return markerTypes[(size_t)it->second.second]; case TypeRole: return it->second.second; } return QVariant(); } int MarkerListModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) return 0; return static_cast(m_markerList.size()); } CommentedTime MarkerListModel::getMarker(const GenTime &pos, bool *ok) const { READ_LOCK(); if (m_markerList.count(pos) <= 0) { // return empty marker *ok = false; return CommentedTime(); } *ok = true; CommentedTime t(pos, m_markerList.at(pos).first, m_markerList.at(pos).second); return t; } QList MarkerListModel::getAllMarkers() const { READ_LOCK(); QList markers; for (const auto &marker : m_markerList) { CommentedTime t(marker.first, marker.second.first, marker.second.second); markers << t; } return markers; } std::vector MarkerListModel::getSnapPoints() const { READ_LOCK(); std::vector markers; for (const auto &marker : m_markerList) { markers.push_back(marker.first.frames(pCore->getCurrentFps())); } return markers; } bool MarkerListModel::hasMarker(int frame) const { READ_LOCK(); return m_markerList.count(GenTime(frame, pCore->getCurrentFps())) > 0; } void MarkerListModel::registerSnapModel(const std::weak_ptr &snapModel) { READ_LOCK(); // make sure ptr is valid if (auto ptr = snapModel.lock()) { // ptr is valid, we store it m_registeredSnaps.push_back(snapModel); // we now add the already existing markers to the snap for (const auto &marker : m_markerList) { - qDebug()<<" *- *-* REGISTEING MARKER: "<getCurrentFps()); + qDebug() << " *- *-* REGISTEING MARKER: " << marker.first.frames(pCore->getCurrentFps()); ptr->addPoint(marker.first.frames(pCore->getCurrentFps())); } } else { qDebug() << "Error: added snapmodel is null"; Q_ASSERT(false); } } bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = importFromJson(data, ignoreConflicts, undo, redo); if (pushUndo) { PUSH_UNDO(undo, redo, m_guide ? i18n("Import guides") : i18n("Import markers")); } return result; } bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } auto list = json.array(); for (const auto &entry : list) { if (!entry.isObject()) { qDebug() << "Warning : Skipping invalid marker data"; continue; } auto entryObj = entry.toObject(); if (!entryObj.contains(QLatin1String("pos"))) { qDebug() << "Warning : Skipping invalid marker data (does not contain position)"; continue; } int pos = entryObj[QLatin1String("pos")].toInt(); QString comment = entryObj[QLatin1String("comment")].toString(i18n("Marker")); int type = entryObj[QLatin1String("type")].toInt(0); if (type < 0 || type >= (int)markerTypes.size()) { qDebug() << "Warning : invalid type found:" << type << " Defaulting to 0"; type = 0; } bool res = true; if (!ignoreConflicts && m_markerList.count(GenTime(pos, pCore->getCurrentFps())) > 0) { // potential conflict found, checking QString oldComment = m_markerList[GenTime(pos, pCore->getCurrentFps())].first; int oldType = m_markerList[GenTime(pos, pCore->getCurrentFps())].second; res = (oldComment == comment) && (type == oldType); } qDebug() << "// ADDING MARKER AT POS: " << pos << ", FPS: " << pCore->getCurrentFps(); res = res && addMarker(GenTime(pos, pCore->getCurrentFps()), comment, type, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QString MarkerListModel::toJson() const { READ_LOCK(); QJsonArray list; for (const auto &marker : m_markerList) { QJsonObject currentMarker; currentMarker.insert(QLatin1String("pos"), QJsonValue(marker.first.frames(pCore->getCurrentFps()))); currentMarker.insert(QLatin1String("comment"), QJsonValue(marker.second.first)); currentMarker.insert(QLatin1String("type"), QJsonValue(marker.second.second)); list.push_back(currentMarker); } QJsonDocument json(list); return QString(json.toJson()); } bool MarkerListModel::removeAllMarkers() { QWriteLocker locker(&m_lock); std::vector all_pos; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (const auto &m : m_markerList) { all_pos.push_back(m.first); } bool res = true; for (const auto &p : all_pos) { res = removeMarker(p, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } PUSH_UNDO(local_undo, local_redo, m_guide ? i18n("Delete all guides") : i18n("Delete all markers")); return true; } bool MarkerListModel::editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ClipController *clip) { bool exists; auto marker = getMarker(pos, &exists); Q_ASSERT(exists || createIfNotFound); if (!exists && createIfNotFound) { marker = CommentedTime(pos, QString()); } QScopedPointer dialog( new MarkerDialog(clip, marker, pCore->bin()->projectTimecode(), m_guide ? i18n("Edit guide") : i18n("Edit marker"), parent)); if (dialog->exec() == QDialog::Accepted) { marker = dialog->newMarker(); if (exists) { return editMarker(pos, marker.time(), marker.comment(), marker.markerType()); } return addMarker(marker.time(), marker.comment(), marker.markerType()); } return false; } diff --git a/src/bin/model/markerlistmodel.hpp b/src/bin/model/markerlistmodel.hpp index 1e8083681..df2eac964 100644 --- a/src/bin/model/markerlistmodel.hpp +++ b/src/bin/model/markerlistmodel.hpp @@ -1,190 +1,192 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef MARKERLISTMODEL_H #define MARKERLISTMODEL_H #include "definitions.h" #include "gentime.h" #include "undohelper.hpp" #include #include #include #include #include class ClipController; class DocUndoStack; class SnapInterface; /* @brief This class is the model for a list of markers. A marker is defined by a time, a type (the color used to represent it) and a comment string. We store them in a sorted fashion using a std::map A marker is essentially bound to a clip. We can also define guides, that are timeline-wise markers. For that, use the constructors without clipId */ class MarkerListModel : public QAbstractListModel { Q_OBJECT public: /* @brief Construct a marker list bound to the bin clip with given id */ explicit MarkerListModel(QString clipId, std::weak_ptr undo_stack, QObject *parent = nullptr); /* @brief Construct a guide list (bound to the timeline) */ MarkerListModel(std::weak_ptr undo_stack, QObject *parent = nullptr); enum { CommentRole = Qt::UserRole + 1, PosRole, FrameRole, ColorRole, TypeRole }; /* @brief Adds a marker at the given position. If there is already one, the comment will be overridden @param pos defines the position of the marker, relative to the clip @param comment is the text associated with the marker @param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults */ bool addMarker(GenTime pos, const QString &comment, int type = -1); protected: /* @brief Same function but accumulates undo/redo */ bool addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo); public: - /* @brief Removes the marker at the given position. */ + /* @brief Removes the marker at the given position. + Returns false if no marker was found at given pos + */ bool removeMarker(GenTime pos); /* @brief Delete all the markers of the model */ bool removeAllMarkers(); protected: /* @brief Same function but accumulates undo/redo */ bool removeMarker(GenTime pos, Fun &undo, Fun &redo); public: /* @brief Edit a marker @param oldPos is the old position of the marker @param pos defines the new position of the marker, relative to the clip @param comment is the text associated with the marker @param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults */ bool editMarker(GenTime oldPos, GenTime pos, QString comment = QString(), int type = -1); /* @brief This describes the available markers type and their corresponding colors */ static std::array markerTypes; /* @brief Returns a marker data at given pos */ CommentedTime getMarker(const GenTime &pos, bool *ok) const; /* @brief Returns all markers in model */ QList getAllMarkers() const; /* @brief Returns all markers positions in model */ std::vector getSnapPoints() const; /* @brief Returns true if a marker exists at given pos Notice that add/remove queries are done in real time (gentime), but this request is made in frame */ Q_INVOKABLE bool hasMarker(int frame) const; /* @brief Registers a snapModel to the marker model. This is intended to be used for a guide model, so that the timelines can register their snapmodel to be updated when the guide moves. This is also used on the clip monitor to keep tracking the clip markers The snap logic for clips is managed from the Timeline Note that no deregistration is necessary, the weak_ptr will be discarded as soon as it becomes invalid. */ void registerSnapModel(const std::weak_ptr &snapModel); /* @brief Exports the model to json using format above */ QString toJson() const; /* @brief Shows a dialog to edit a marker/guide @param pos: position of the marker to edit, or new position for a marker @param widget: qt widget that will be the parent of the dialog @param createIfNotFound: if true, we create a marker if none is found at pos @param clip: pointer to the clip if we are editing a marker @return true if dialog was accepted and modification successful */ bool editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ClipController *clip = nullptr); // Mandatory overloads QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; public slots: /* @brief Imports a list of markers from json data The data should be formatted as follows: [{"pos":0.2, "comment":"marker 1", "type":1}, {...}, ...] return true on success and logs undo object @param ignoreConflicts: if set to false, it aborts if the data contains a marker with same position but different comment and/or type. If set to true, such markers are overridden silently @param pushUndo: if true, create an undo object */ bool importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo = true); bool importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo); protected: /* @brief Adds a snap point at marker position in the registered snap models (those that are still valid)*/ void addSnapPoint(GenTime pos); /* @brief Deletes a snap point at marker position in the registered snap models (those that are still valid)*/ void removeSnapPoint(GenTime pos); /** @brief Helper function that generate a lambda to change comment / type of given marker */ Fun changeComment_lambda(GenTime pos, const QString &comment, int type); /** @brief Helper function that generate a lambda to add given marker */ Fun addMarker_lambda(GenTime pos, const QString &comment, int type); /** @brief Helper function that generate a lambda to remove given marker */ Fun deleteMarker_lambda(GenTime pos); /** @brief Helper function that retrieves a pointer to the markermodel, given whether it's a guide model and its clipId*/ static std::shared_ptr getModel(bool guide, const QString &clipId); /* @brief Connects the signals of this object */ void setup(); private: std::weak_ptr m_undoStack; bool m_guide; // whether this model represents timeline-wise guides QString m_clipId; // the Id of the clip this model corresponds to, if any. mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::map> m_markerList; std::vector> m_registeredSnaps; signals: void modelChanged(); public: // this is to enable for range loops auto begin() -> decltype(m_markerList.begin()) { return m_markerList.begin(); } auto end() -> decltype(m_markerList.end()) { return m_markerList.end(); } }; Q_DECLARE_METATYPE(MarkerListModel *) #endif diff --git a/tests/markertest.cpp b/tests/markertest.cpp index 214e58187..4c00ff00e 100644 --- a/tests/markertest.cpp +++ b/tests/markertest.cpp @@ -1,193 +1,220 @@ -#include "catch.hpp" - -#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" -#pragma GCC diagnostic push -#include "fakeit.hpp" -#include -#include -#include -#include -#include -#include -#include +#include "test_utils.hpp" +#include "kdenlivesettings.h" #define private public #define protected public #include "bin/model/markerlistmodel.hpp" -#include "core.h" -#include "doc/docundostack.hpp" -#include "gentime.h" -#include "project/projectmanager.h" #include "timeline2/model/snapmodel.hpp" -using namespace fakeit; - using Marker = std::tuple; double fps; void checkMarkerList(const std::shared_ptr &model, const std::vector &l, const std::shared_ptr &snaps) { auto list = l; std::sort(list.begin(), list.end(), [](const Marker &a, const Marker &b) { return std::get<0>(a) < std::get<0>(b); }); REQUIRE(model->rowCount() == (int)list.size()); if (model->rowCount() == 0) { REQUIRE(snaps->getClosestPoint(0) == -1); } for (int i = 0; i < model->rowCount(); ++i) { REQUIRE(qAbs(std::get<0>(list[i]).seconds() - model->data(model->index(i), MarkerListModel::PosRole).toDouble()) < 0.9 / fps); REQUIRE(std::get<1>(list[i]) == model->data(model->index(i), MarkerListModel::CommentRole).toString()); REQUIRE(std::get<2>(list[i]) == model->data(model->index(i), MarkerListModel::TypeRole).toInt()); REQUIRE(MarkerListModel::markerTypes[std::get<2>(list[i])] == model->data(model->index(i), MarkerListModel::ColorRole).value()); // check for marker existence int frame = std::get<0>(list[i]).frames(fps); REQUIRE(model->hasMarker(frame)); // cheap way to check for snap REQUIRE(snaps->getClosestPoint(frame) == frame); } } void checkStates(const std::shared_ptr &undoStack, const std::shared_ptr &model, const std::vector> &states, const std::shared_ptr &snaps) { for (size_t i = 0; i < states.size(); ++i) { checkMarkerList(model, states[states.size() - 1 - i], snaps); - if (i < states.size() - 1) { + if (i != states.size() - 1) { undoStack->undo(); } } for (size_t i = 1; i < states.size(); ++i) { undoStack->redo(); checkMarkerList(model, states[i], snaps); } } TEST_CASE("Marker model", "[MarkerListModel]") { fps = pCore->getCurrentFps(); GenTime::setFps(fps); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr model = std::make_shared(undoStack, nullptr); std::shared_ptr snaps = std::make_shared(); model->registerSnapModel(snaps); // Here we do some trickery to enable testing. // We mock the project class so that the getGuideModel function returns this model Mock pmMock; When(Method(pmMock, getGuideModel)).AlwaysReturn(model); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; SECTION("Basic Manipulation") { std::vector list; checkMarkerList(model, list, snaps); // add markers list.emplace_back(GenTime(1.3), QLatin1String("test marker"), 3); - model->addMarker(GenTime(1.3), QLatin1String("test marker"), 3); + REQUIRE(model->addMarker(GenTime(1.3), QLatin1String("test marker"), 3)); checkMarkerList(model, list, snaps); auto state1 = list; checkStates(undoStack, model, {{}, state1}, snaps); list.emplace_back(GenTime(0.3), QLatin1String("test marker2"), 0); - model->addMarker(GenTime(0.3), QLatin1String("test marker2"), 0); + REQUIRE(model->addMarker(GenTime(0.3), QLatin1String("test marker2"), 0)); checkMarkerList(model, list, snaps); auto state2 = list; checkStates(undoStack, model, {{}, state1, state2}, snaps); + // delete unexisting marker shouldn't work + REQUIRE_FALSE(model->removeMarker(GenTime(42.))); + checkMarkerList(model, list, snaps); + checkStates(undoStack, model, {{}, state1, state2}, snaps); + // rename markers std::get<1>(list[0]) = QLatin1String("new comment"); std::get<2>(list[0]) = 1; - model->addMarker(GenTime(1.3), QLatin1String("new comment"), 1); + REQUIRE(model->addMarker(GenTime(1.3), QLatin1String("new comment"), 1)); checkMarkerList(model, list, snaps); auto state3 = list; checkStates(undoStack, model, {{}, state1, state2, state3}, snaps); - // delete markers - std::swap(list[0], list[1]); - list.pop_back(); - model->removeMarker(GenTime(1.3)); + // edit marker + GenTime oldPos = std::get<0>(list[1]); + std::get<0>(list[1]) = GenTime(42.8); + std::get<1>(list[1]) = QLatin1String("edited comment"); + std::get<2>(list[1]) = 3; + REQUIRE(model->editMarker(oldPos, GenTime(42.8), QLatin1String("edited comment"), 3)); checkMarkerList(model, list, snaps); auto state4 = list; checkStates(undoStack, model, {{}, state1, state2, state3, state4}, snaps); + // delete markers + std::swap(list[0], list[1]); list.pop_back(); - model->removeMarker(GenTime(0.3)); + REQUIRE(model->removeMarker(GenTime(1.3))); checkMarkerList(model, list, snaps); auto state5 = list; checkStates(undoStack, model, {{}, state1, state2, state3, state4, state5}, snaps); + + GenTime old = std::get<0>(list.back()); + list.pop_back(); + REQUIRE(model->removeMarker(old)); + checkMarkerList(model, list, snaps); + auto state6 = list; + checkStates(undoStack, model, {{}, state1, state2, state3, state4, state5, state6}, snaps); + + // add some back + list.emplace_back(GenTime(1.7), QLatin1String("test marker6"), KdenliveSettings::default_marker_type()); + REQUIRE(model->addMarker(GenTime(1.7), QLatin1String("test marker6"), -1)); + auto state7 = list; + list.emplace_back(GenTime(2), QLatin1String("auieuansr"), 3); + REQUIRE(model->addMarker(GenTime(2), QLatin1String("auieuansr"), 3)); + auto state8 = list; + list.emplace_back(GenTime(0), QLatin1String("sasenust"), 1); + REQUIRE(model->addMarker(GenTime(0), QLatin1String("sasenust"), 1)); + checkMarkerList(model, list, snaps); + auto state9 = list; + checkStates(undoStack, model, {{}, state1, state2, state3, state4, state5, state6, state7, state8, state9}, snaps); + + // try spurious model registration + std::shared_ptr spurious; + REQUIRE(ABORTS(&MarkerListModel::registerSnapModel, model, spurious)); + + // try real model registration + std::shared_ptr other_snaps = std::make_shared(); + model->registerSnapModel(other_snaps); + checkMarkerList(model, list, other_snaps); + + // remove all + REQUIRE(model->removeAllMarkers()); + checkMarkerList(model, {}, snaps); + checkStates(undoStack, model, {{}, state1, state2, state3, state4, state5, state6, state7, state8, state9, {}}, snaps); } SECTION("Json identity test") { std::vector list; checkMarkerList(model, list, snaps); // add markers list.emplace_back(GenTime(1.3), QLatin1String("test marker"), 3); model->addMarker(GenTime(1.3), QLatin1String("test marker"), 3); list.emplace_back(GenTime(0.3), QLatin1String("test marker2"), 0); model->addMarker(GenTime(0.3), QLatin1String("test marker2"), 0); list.emplace_back(GenTime(3), QLatin1String("test marker3"), 0); model->addMarker(GenTime(3), QLatin1String("test marker3"), 0); checkMarkerList(model, list, snaps); // export QString json = model->toJson(); // clean model->removeMarker(GenTime(0.3)); model->removeMarker(GenTime(3)); model->removeMarker(GenTime(1.3)); checkMarkerList(model, {}, snaps); // Reimport REQUIRE(model->importFromJson(json, false)); checkMarkerList(model, list, snaps); // undo/redo undoStack->undo(); checkMarkerList(model, {}, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); // now we try the same thing with non-empty model undoStack->undo(); checkMarkerList(model, {}, snaps); // non - conflicting marker list.emplace_back(GenTime(5), QLatin1String("non conflicting"), 0); std::vector otherMarkers; otherMarkers.emplace_back(GenTime(5), QLatin1String("non conflicting"), 0); model->addMarker(GenTime(5), QLatin1String("non conflicting"), 0); REQUIRE(model->importFromJson(json, false)); checkMarkerList(model, list, snaps); undoStack->undo(); checkMarkerList(model, otherMarkers, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); undoStack->undo(); // conflicting marker otherMarkers.emplace_back(GenTime(1.3), QLatin1String("conflicting"), 1); model->addMarker(GenTime(1.3), QLatin1String("conflicting"), 1); checkMarkerList(model, otherMarkers, snaps); REQUIRE_FALSE(model->importFromJson(json, false)); checkMarkerList(model, otherMarkers, snaps); REQUIRE(model->importFromJson(json, true)); checkMarkerList(model, list, snaps); undoStack->undo(); checkMarkerList(model, otherMarkers, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); } pCore->m_projectManager = nullptr; } diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 43134f705..8df8d3a41 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -1,87 +1,88 @@ #pragma once +#include "abortutil.hpp" #include "bin/model/markerlistmodel.hpp" #include "catch.hpp" #include "doc/docundostack.hpp" #include #include #include #include #include "logger.hpp" #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #pragma GCC diagnostic push #include "fakeit.hpp" #include #include #include #include #define private public #define protected public #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectitemmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelinefunctions.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/timelinemodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "transitions/transitionsrepository.hpp" using namespace fakeit; #define RESET(mock) \ mock.Reset(); \ Fake(Method(mock, adjustAssetRange)); \ Spy(Method(mock, _resetView)); \ Spy(Method(mock, _beginInsertRows)); \ Spy(Method(mock, _beginRemoveRows)); \ Spy(Method(mock, _endInsertRows)); \ Spy(Method(mock, _endRemoveRows)); \ Spy(OverloadedMethod(mock, notifyChange, void(const QModelIndex &, const QModelIndex &, bool, bool, bool))); \ Spy(OverloadedMethod(mock, notifyChange, void(const QModelIndex &, const QModelIndex &, const QVector &))); \ Spy(OverloadedMethod(mock, notifyChange, void(const QModelIndex &, const QModelIndex &, int))); #define NO_OTHERS() \ VerifyNoOtherInvocations(Method(timMock, _beginRemoveRows)); \ VerifyNoOtherInvocations(Method(timMock, _beginInsertRows)); \ VerifyNoOtherInvocations(Method(timMock, _endRemoveRows)); \ VerifyNoOtherInvocations(Method(timMock, _endInsertRows)); \ VerifyNoOtherInvocations(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, bool, bool, bool))); \ VerifyNoOtherInvocations(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, const QVector &))); \ RESET(timMock); #define CHECK_MOVE(times) \ Verify(Method(timMock, _beginRemoveRows) + Method(timMock, _endRemoveRows) + Method(timMock, _beginInsertRows) + Method(timMock, _endInsertRows)) \ .Exactly(times); \ NO_OTHERS(); #define CHECK_INSERT(times) \ Verify(Method(timMock, _beginInsertRows) + Method(timMock, _endInsertRows)).Exactly(times); \ NO_OTHERS(); #define CHECK_REMOVE(times) \ Verify(Method(timMock, _beginRemoveRows) + Method(timMock, _endRemoveRows)).Exactly(times); \ NO_OTHERS(); #define CHECK_RESIZE(times) \ Verify(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, bool, bool, bool))).Exactly(times); \ NO_OTHERS(); #define CHECK_UPDATE(role) \ Verify(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, int)) \ .Matching([](const QModelIndex &, const QModelIndex &, int c) { return c == role; })) \ .Exactly(1); \ NO_OTHERS(); QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr binModel, int length = 20, bool limited = true); QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr binModel);