diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0dcff01ed..1d30b7029 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,356 +1,358 @@ add_definitions(${Qt5Gui_DEFINITIONS}) if(${Qt5Gui_OPENGL_IMPLEMENTATION} STREQUAL "GL") find_package(OpenGL REQUIRED) set_package_properties(OpenGL PROPERTIES DESCRIPTION "the OpenGL library" URL "" TYPE RUNTIME PURPOSE "") else() find_package(OpenGLES REQUIRED) set_package_properties(OpenGLES PROPERTIES DESCRIPTION "the OpenGLES library" URL "" TYPE RUNTIME PURPOSE "") endif() +SET(CMAKE_CXX_STANDARD 14) +SET(CMAKE_CXX_STANDARD_REQUIRED ON) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Wall -pedantic -Wextra") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wcast-qual -Wcast-align -Wfloat-equal -Wpointer-arith") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunreachable-code -Wchar-subscripts -Wcomment -Wformat") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror-implicit-function-declaration -Wmain -Wmissing-braces") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wparentheses -Wsequence-point -Wreturn-type -Wswitch") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wuninitialized -Wreorder -Wundef -Wshadow -Wwrite-strings") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wconversion") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wmissing-noreturn -Wsign-conversion -Wunused ") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wstrict-aliasing -Wstrict-overflow -Wconversion") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wdisabled-optimization") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-undef") if (CMAKE_COMPILER_IS_GNUCXX) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wlogical-op -Wunsafe-loop-optimizations ") endif() SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunused-parameter -Wshadow -Wno-variadic-macros -Wno-float-conversion") find_package(PkgConfig QUIET) execute_process( COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=mltdatadir mlt-framework OUTPUT_VARIABLE MLT_DATADIR RESULT_VARIABLE MLT_DATADIR_failed) if (NOT MLT_DATADIR_failed) string(REGEX REPLACE "[\r\n]" "" MLT_DATADIR "${MLT_DATADIR}") endif() execute_process( COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=meltbin mlt-framework OUTPUT_VARIABLE MLT_MELTBIN RESULT_VARIABLE MLT_MELTBIN_failed) if (NOT MLT_MELTBIN_failed) string(REGEX REPLACE "[\r\n]" "" MLT_MELTBIN "${MLT_MELTBIN}") endif() configure_file( mlt_config.h.in ${CMAKE_BINARY_DIR}/generated/mlt_config.h ) include_directories( ${CMAKE_BINARY_DIR}/generated/ ) # Make sure it can be included... option(WITH_JogShuttle "Build Jog/Shuttle support" ON) set(FFMPEG_SUFFIX "" CACHE STRING "FFmpeg custom suffix") find_package(LibV4L2) set_package_properties(LibV4L2 PROPERTIES DESCRIPTION "Collection of video4linux support libraries" URL "http://freecode.com/projects/libv4l" TYPE RUNTIME PURPOSE "Required for better webcam support") if(WITH_JogShuttle) check_include_files(linux/input.h HAVE_LINUX_INPUT_H) if(HAVE_LINUX_INPUT_H) set(BUILD_JogShuttle TRUE) endif(HAVE_LINUX_INPUT_H) endif() set_package_properties(OpenGL PROPERTIES DESCRIPTION "the OpenGL library" URL "" TYPE RUNTIME PURPOSE "") #if(APPLE) # macro_log_feature(SDL_FOUND # "SDL" # "Cross-platform multimedia library" # "http://www.libsdl.org" # TRUE # ) #endif(APPLE) # use sane compile flags add_definitions( # -DQT_USE_QSTRINGBUILDER -DQT_NO_CAST_TO_ASCII # -DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_BYTEARRAY # -DQT_USE_FAST_OPERATOR_PLUS ) install(FILES kdenlivesettings.kcfg DESTINATION ${KCFG_INSTALL_DIR}) kconfig_add_kcfg_files(kdenlive_SRCS kdenlivesettings.kcfgc) add_subdirectory(abstractmodel) add_subdirectory(assets) add_subdirectory(bin) add_subdirectory(capture) add_subdirectory(dialogs) add_subdirectory(doc) add_subdirectory(dvdwizard) add_subdirectory(effects) add_subdirectory(effectslist) add_subdirectory(jobs) add_subdirectory(lib) add_subdirectory(mltcontroller) add_subdirectory(monitor) add_subdirectory(profiles) add_subdirectory(project) add_subdirectory(qml) add_subdirectory(scopes) add_subdirectory(simplekeyframes) add_subdirectory(timeline2) add_subdirectory(titler) add_subdirectory(transitions) add_subdirectory(utils) add_subdirectory(widgets) add_subdirectory(xml) if (Qt5WebKitWidgets_FOUND) add_subdirectory(qt-oauth-lib) endif() add_subdirectory(library) list(APPEND kdenlive_SRCS colortools.cpp definitions.cpp gentime.cpp doc/kthumb.cpp mainwindow.cpp renderer.cpp statusbarmessagelabel.cpp timecode.cpp timecodedisplay.cpp layoutmanagement.cpp hidetitlebars.cpp mltconnection.cpp core.cpp undohelper.cpp ) ecm_qt_declare_logging_category(kdenlive_SRCS HEADER kdenlive_debug.h IDENTIFIER KDENLIVE_LOG CATEGORY_NAME org.kde.multimedia.kdenlive) ki18n_wrap_ui(kdenlive_UIS ui/addtrack_ui.ui ui/archivewidget_ui.ui ui/audiospectrum_ui.ui ui/backupdialog_ui.ui ui/bezierspline_ui.ui ui/boolparamwidget_ui.ui ui/clipdurationdialog_ui.ui ui/clipproperties_ui.ui ui/clipspeed_ui.ui ui/clipstabilize_ui.ui ui/cliptranscode_ui.ui ui/collapsiblewidget_ui.ui ui/colorclip_ui.ui ui/colorplaneexport_ui.ui ui/configcapture_ui.ui ui/configenv_ui.ui ui/configjogshuttle_ui.ui ui/configmisc_ui.ui ui/configproject_ui.ui ui/configsdl_ui.ui ui/configtimeline_ui.ui ui/configtranscode_ui.ui ui/cutjobdialog_ui.ui ui/dvdwizardchapters_ui.ui ui/dvdwizardmenu_ui.ui ui/dvdwizardstatus_ui.ui ui/dvdwizardvob_ui.ui ui/effectlist_ui.ui ui/fontval_ui.ui ui/freesound_ui.ui ui/geometrywidget_ui.ui ui/gradientedit_ui.ui ui/histogram_ui.ui ui/keyframedialog_ui.ui ui/keyframeeditor_ui.ui ui/keyframewidget_ui.ui ui/keywordval_ui.ui ui/listparamwidget_ui.ui ui/logindialog_ui.ui ui/managecaptures_ui.ui ui/manageencodingprofile_ui.ui ui/markerdialog_ui.ui ui/missingclips_ui.ui ui/monitoreditwidget_ui.ui ui/profiledialog_ui.ui ui/projectsettings_ui.ui ui/qtextclip_ui.ui ui/recmonitor_ui.ui ui/renderwidget_ui.ui ui/rgbparade_ui.ui ui/saveprofile_ui.ui ui/scenecutdialog_ui.ui ui/selectivecolor_ui.ui ui/slideshowclip_ui.ui ui/smconfig_ui.ui ui/spacerdialog_ui.ui ui/spectrogram_ui.ui ui/templateclip_ui.ui ui/titlewidget_ui.ui ui/tracksconfigdialog_ui.ui ui/transitionsettings_ui.ui ui/unicodewidget_ui.ui ui/urlval_ui.ui ui/vectorscope_ui.ui ui/waveform_ui.ui ui/wipeval_ui.ui ui/wizardcapture_ui.ui ui/wizardcheck_ui.ui ui/wizardextra_ui.ui ui/wizardmltcheck_ui.ui ui/wizardstandard_ui.ui ) if(BUILD_JogShuttle) list(APPEND kdenlive_SRCS jogshuttle/jogmanager.cpp jogshuttle/jogaction.cpp jogshuttle/jogshuttle.cpp jogshuttle/jogshuttleconfig.cpp ) endif() # Sets the icon on Windows and OSX file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/../data/icons/*-apps-kdenlive.png") ecm_add_app_icon(kdenlive_SRCS ICONS ${ICONS_SRCS}) qt5_add_dbus_adaptor(kdenlive_SRCS org.kdenlive.MainWindow.xml mainwindow.h MainWindow ) qt5_add_resources(kdenlive_extra_SRCS icons.qrc ui/resources.qrc uiresources.qrc) qt5_wrap_cpp(kdenlive_MOC definitions.h) add_library(kdenliveLib STATIC ${kdenlive_SRCS} ${kdenlive_UIS} ${kdenlive_MOC}) add_executable(kdenlive main.cpp ${kdenlive_extra_SRCS} ) target_link_libraries(kdenlive kdenliveLib ) # To compile kiss_fft. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --std=c99") # KDE definitions and include directories *must* always come first, Qt follows # (to avoid breaking builds when KDE and/or Qt are installed to different # prefixes). include_directories( ${CMAKE_BINARY_DIR} ${MLT_INCLUDE_DIR} ${MLTPP_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/lib/external ${CMAKE_CURRENT_SOURCE_DIR}/lib ) # Adds Qt definitions and include directories, and sets QT_LIBRARIES according # to the components requested in find_package(). #include(${QT_USE_FILE}) target_link_libraries(kdenliveLib KF5::WidgetsAddons KF5::Archive KF5::CoreAddons KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::NotifyConfig KF5::NewStuff KF5::DBusAddons KF5::XmlGui KF5::GuiAddons KF5::Notifications KF5::TextWidgets KF5::Declarative KF5::IconThemes KF5::Solid Qt5::Svg Qt5::Quick Qt5::QuickWidgets Qt5::Concurrent ${OPENGL_LIBRARIES} ${OPENGLES_LIBRARIES} ${MLT_LIBRARIES} ${MLTPP_LIBRARIES} ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT} kiss_fft ) message(STATUS "Found MLT++: ${MLTPP_LIBRARIES}") if (KF5_FILEMETADATA) add_definitions(-DKF5_USE_FILEMETADATA) target_link_libraries(kdenliveLib KF5::FileMetaData) endif() if (KF5Crash_FOUND) add_definitions(-DKF5_USE_CRASH) target_link_libraries(kdenliveLib KF5::Crash) endif() if (KF5_PURPOSE) add_definitions(-DKF5_USE_PURPOSE) target_link_libraries(kdenliveLib KF5::Purpose KF5::PurposeWidgets) endif() if (Qt5WebKitWidgets_FOUND) message(STATUS "Found Qt5 WebKitWidgets. You can use your Freesound.org credentials to download files") add_definitions(-DQT5_USE_WEBKIT) target_link_libraries(kdenliveLib Qt5::WebKitWidgets) else() message(STATUS "Qt5 WebKitWidgets not found. You cannot use your Freesound.org credentials, only preview files can be downloaded from the Online Resources Widget") endif() if(Q_WS_X11) include_directories(${X11_Xlib_INCLUDE_PATH}) target_link_libraries(kdenliveLib ${X11_LIBRARIES}) endif(Q_WS_X11) if(SDL2_FOUND) target_link_libraries(kdenliveLib ${SDL2_LIBRARY}) elseif(SDL_FOUND) target_link_libraries(kdenliveLib ${SDL_LIBRARY}) endif(SDL2_FOUND) if(LIBV4L2_FOUND) include_directories(${LIBV4L2_INCLUDE_DIR}) target_link_libraries(kdenliveLib ${LIBV4L2_LIBRARY}) add_definitions(-DUSE_V4L) endif() if(BUILD_JogShuttle) add_definitions(-DUSE_JOGSHUTTLE) target_link_libraries(kdenliveLib media_ctrl ) endif() set_property(SOURCE definitions.h PROPERTY SKIP_AUTOMOC ON) install(TARGETS kdenlive DESTINATION ${BIN_INSTALL_DIR}) install(FILES kdenliveui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/kdenlive) diff --git a/src/bin/abstractprojectitem.cpp b/src/bin/abstractprojectitem.cpp index 235a0abfe..514a2143e 100644 --- a/src/bin/abstractprojectitem.cpp +++ b/src/bin/abstractprojectitem.cpp @@ -1,297 +1,297 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 "abstractprojectitem.h" #include "bin.h" #include "core.h" #include "jobs/jobmanager.h" #include "macros.hpp" #include "projectitemmodel.h" #include "jobs/audiothumbjob.hpp" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include #include #include AbstractProjectItem::AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const std::shared_ptr &model, bool isRoot) : TreeItem(QList(), std::static_pointer_cast(model), isRoot) , m_name() , m_description() , m_thumbnail(QIcon()) , m_date() , m_binId(id) , m_usage(0) , m_clipStatus(StatusReady) , m_itemType(type) , m_lock(QReadWriteLock::Recursive) , m_isCurrent(false) { Q_ASSERT(!isRoot || type == FolderItem); } bool AbstractProjectItem::operator==(const std::shared_ptr &projectItem) const { // FIXME: only works for folders bool equal = this->m_childItems == projectItem->m_childItems; // equal = equal && (m_parentItem == projectItem->m_parentItem); return equal; } std::shared_ptr AbstractProjectItem::parent() const { return std::static_pointer_cast(m_parentItem.lock()); } void AbstractProjectItem::setRefCount(uint count) { m_usage = count; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } uint AbstractProjectItem::refCount() const { return m_usage; } void AbstractProjectItem::addRef() { m_usage++; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } void AbstractProjectItem::removeRef() { m_usage--; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } const QString &AbstractProjectItem::clipId() const { return m_binId; } QPixmap AbstractProjectItem::roundedPixmap(const QPixmap &source) { QPixmap pix(source.width(), source.height()); pix.fill(Qt::transparent); QPainter p(&pix); p.setRenderHint(QPainter::Antialiasing, true); QPainterPath path; path.addRoundedRect(0.5, 0.5, pix.width() - 1, pix.height() - 1, 4, 4); p.setClipPath(path); p.drawPixmap(0, 0, source); p.end(); return pix; } AbstractProjectItem::PROJECTITEMTYPE AbstractProjectItem::itemType() const { return m_itemType; } QVariant AbstractProjectItem::getData(DataType type) const { QVariant data; switch (type) { case DataName: data = QVariant(m_name); break; case DataDescription: data = QVariant(m_description); break; case DataThumbnail: data = QVariant(m_thumbnail); break; case DataId: data = QVariant(m_id); break; case DataDuration: data = QVariant(m_duration); break; case DataInPoint: data = QVariant(m_inPoint); break; case DataDate: data = QVariant(m_date); break; case UsageCount: data = QVariant(m_usage); break; case ItemTypeRole: data = QVariant(m_itemType); break; case JobType: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.empty()) { jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); } if (jobIds.size() > 0) { data = QVariant(pCore->jobManager()->getJobType(jobIds[0])); } } break; case JobStatus: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.empty()) { jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); } if (jobIds.size() > 0) { data = QVariant::fromValue(pCore->jobManager()->getJobStatus(jobIds[0])); } else { data = QVariant::fromValue(JobManagerStatus::NoJob); } } break; case JobProgress: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.size() > 0) { data = QVariant(pCore->jobManager()->getJobProgressForClip(jobIds[0], clipId())); } else { data = QVariant(0); } } break; case JobMessage: if (itemType() == ClipItem) { QString messages; auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); for (int job : jobIds) { messages.append(pCore->jobManager()->getJobMessageForClip(job, clipId())); } jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); for (int job : jobIds) { messages.append(pCore->jobManager()->getJobMessageForClip(job, clipId())); } data = QVariant(messages); } break; case ClipStatus: data = QVariant(m_clipStatus); break; case ClipToolTip: data = QVariant(getToolTip()); break; default: break; } return data; } int AbstractProjectItem::supportedDataCount() const { return 3; } QString AbstractProjectItem::name() const { return m_name; } void AbstractProjectItem::setName(const QString &name) { m_name = name; } QString AbstractProjectItem::description() const { return m_description; } void AbstractProjectItem::setDescription(const QString &description) { m_description = description; } QPoint AbstractProjectItem::zone() const { return QPoint(); } void AbstractProjectItem::setClipStatus(CLIPSTATUS status) { m_clipStatus = status; } bool AbstractProjectItem::statusReady() const { return m_clipStatus == StatusReady; } AbstractProjectItem::CLIPSTATUS AbstractProjectItem::clipStatus() const { return m_clipStatus; } std::shared_ptr AbstractProjectItem::getEnclosingFolder(bool strict) { if (!strict && itemType() == AbstractProjectItem::FolderItem) { return std::static_pointer_cast(shared_from_this()); } if (auto ptr = m_parentItem.lock()) { return std::static_pointer_cast(ptr)->getEnclosingFolder(false); } return std::shared_ptr(); } -bool AbstractProjectItem::selfSoftDelete(Fun &undo, Fun &redo) +bool AbstractProjectItem::selfSoftDelete(Fun &undo, Fun &redo, Updates &list) { pCore->jobManager()->slotDiscardClipJobs(clipId()); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (const auto &child : m_childItems) { - bool res = std::static_pointer_cast(child)->selfSoftDelete(local_undo, local_redo); + bool res = std::static_pointer_cast(child)->selfSoftDelete(local_undo, local_redo, list); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } QString AbstractProjectItem::lastParentId() const { return m_lastParentId; } void AbstractProjectItem::updateParent(std::shared_ptr newParent) { // bool reload = !m_lastParentId.isEmpty(); m_lastParentId.clear(); if (newParent) { m_lastParentId = std::static_pointer_cast(newParent)->clipId(); } TreeItem::updateParent(newParent); } diff --git a/src/bin/abstractprojectitem.h b/src/bin/abstractprojectitem.h index 66933491d..5500deccf 100644 --- a/src/bin/abstractprojectitem.h +++ b/src/bin/abstractprojectitem.h @@ -1,218 +1,219 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 ABSTRACTPROJECTITEM_H #define ABSTRACTPROJECTITEM_H #include "abstractmodel/treeitem.hpp" +#include "timeline2/model/modelupdater.hpp" #include "undohelper.hpp" #include #include #include #include class ProjectClip; class ProjectFolder; class Bin; class QDomElement; class QDomDocument; class ProjectItemModel; /** * @class AbstractProjectItem * @brief Base class for all project items (clips, folders, ...). * * Project items are stored in a tree like structure ... */ class AbstractProjectItem : public QObject, public TreeItem { Q_OBJECT public: enum PROJECTITEMTYPE { FolderUpItem = 0, FolderItem = 1, ClipItem = 2, SubClipItem = 3 }; /** * @brief Constructor. * @param type is the type of the bin item * @param id is the binId * @param model is the ptr to the item model * @param isRoot is true if this is the topmost folder */ AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const std::shared_ptr &model, bool isRoot = false); bool operator==(const std::shared_ptr &projectItem) const; /** @brief Returns a pointer to the parent item (or NULL). */ std::shared_ptr parent() const; /** @brief Returns the type of this item (folder, clip, subclip, etc). */ PROJECTITEMTYPE itemType() const; /** @brief Used to search for a clip with a specific id. */ virtual std::shared_ptr clip(const QString &id) = 0; /** @brief Used to search for a folder with a specific id. */ virtual std::shared_ptr folder(const QString &id) = 0; virtual std::shared_ptr clipAt(int ix) = 0; /** @brief Recursively disable/enable bin effects. */ virtual void setBinEffectsEnabled(bool enabled) = 0; /** @brief This function executes what should be done when the item is deleted but without deleting effectively. For example, the item will deregister itself from the model and delete the clips from the timeline. However, the object is NOT actually deleted, and the tree structure is preserved. @param Undo,Redo are the lambdas accumulating the update. */ - virtual bool selfSoftDelete(Fun &undo, Fun &redo); + virtual bool selfSoftDelete(Fun &undo, Fun &redo, Updates &list); /** @brief Returns the clip's id. */ const QString &clipId() const; virtual QPoint zone() const; // TODO refac : these ref counting are probably deprecated by smart ptrs /** @brief Set current usage count. */ void setRefCount(uint count); /** @brief Returns clip's current usage count in timeline. */ uint refCount() const; /** @brief Increase usage count. */ void addRef(); /** @brief Decrease usage count. */ void removeRef(); enum DataType { // display name of item DataName = Qt::DisplayRole, // image thumbnail DataThumbnail = Qt::DecorationRole, // Tooltip text,usually full path ClipToolTip = Qt::ToolTipRole, // unique id of the project clip / folder DataId = Qt::UserRole, // creation date DataDate, // Description for item (user editable) DataDescription, // Number of occurrences used in timeline UsageCount, // Empty if clip has no effect, icon otherwise IconOverlay, // item type (clip, subclip, folder) ItemTypeRole, // Duration of the clip DataDuration, // Inpoint of the subclip (0 for clips) DataInPoint, // If there is a running job, which type JobType, // Current progress of the job JobProgress, // error message if job crashes (not fully implemented) JobMessage, JobStatus, // Item status (ready or not, missing, waiting, ...) ClipStatus }; enum CLIPSTATUS { StatusReady = 0, StatusMissing, StatusWaiting, StatusDeleting }; void setClipStatus(AbstractProjectItem::CLIPSTATUS status); AbstractProjectItem::CLIPSTATUS clipStatus() const; bool statusReady() const; /** @brief Returns the data that describes this item. * @param type type of data to return * * This function is necessary for interaction with ProjectItemModel. */ virtual QVariant getData(DataType type) const; /** * @brief Returns the amount of different types of data this item supports. * * This base class supports only DataName and DataDescription, so the return value is always 2. * This function is necessary for interaction with ProjectItemModel. */ virtual int supportedDataCount() const; /** @brief Returns the (displayable) name of this item. */ QString name() const; /** @brief Sets a new (displayable) name. */ virtual void setName(const QString &name); /** @brief Returns the (displayable) description of this item. */ QString description() const; /** @brief Sets a new description. */ virtual void setDescription(const QString &description); virtual QDomElement toXml(QDomDocument &document, bool includeMeta = false) = 0; virtual QString getToolTip() const = 0; virtual bool rename(const QString &name, int column) = 0; /* @brief Return the bin id of the last parent that this element got, even if this parent has already been destroyed. Return the empty string if the element was parentless */ QString lastParentId() const; /* @brief This is an overload of TreeItem::updateParent that tracks the id of the id of the parent */ void updateParent(std::shared_ptr newParent) override; /* Returns a ptr to the enclosing dir, and nullptr if none is found. @param strict if set to false, the enclosing dir of a dir is itself, otherwise we try to find a "true" parent */ std::shared_ptr getEnclosingFolder(bool strict = false); /** @brief Returns true if a clip corresponding to this bin is inserted in a timeline. Note that this function does not account for children, use TreeItem::accumulate if you want to get that information as well. */ virtual bool isIncludedInTimeline() { return false; } signals: void childAdded(AbstractProjectItem *child); void aboutToRemoveChild(AbstractProjectItem *child); protected: QString m_name; QString m_description; QIcon m_thumbnail; QString m_duration; int m_inPoint; QDateTime m_date; QString m_binId; uint m_usage; CLIPSTATUS m_clipStatus; PROJECTITEMTYPE m_itemType; QString m_lastParentId; /** @brief Returns a rounded border pixmap from the @param source pixmap. */ QPixmap roundedPixmap(const QPixmap &source); mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access private: bool m_isCurrent; }; #endif diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index f04e2326a..204c03958 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3138 +1,3142 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 "bin.h" #include "bincommands.h" #include "clipcreator.hpp" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "doc/documentchecker.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mlt++/Mlt.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "monitor/monitor.h" #include "project/dialogs/slideshowclip.h" #include "project/invaliddialog.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectfolderup.h" #include "projectitemmodel.h" #include "projectsortproxymodel.h" #include "projectsubclip.h" +#include "timeline2/model/modelupdater.hpp" #include "titler/titlewidget.h" #include "ui_qtextclip_ui.h" #include "undohelper.hpp" #include "xml/xml.hpp" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include MyListView::MyListView(QWidget *parent) : QListView(parent) { setViewMode(QListView::IconMode); setMovement(QListView::Static); setResizeMode(QListView::Adjust); setUniformItemSizes(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDragEnabled(true); viewport()->setAcceptDrops(true); } void MyListView::focusInEvent(QFocusEvent *event) { QListView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } MyTreeView::MyTreeView(QWidget *parent) : QTreeView(parent) { setEditing(false); } void MyTreeView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_startPos = event->pos(); } QTreeView::mousePressEvent(event); } void MyTreeView::focusInEvent(QFocusEvent *event) { QTreeView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } void MyTreeView::mouseMoveEvent(QMouseEvent *event) { bool dragged = false; if ((event->buttons() & Qt::LeftButton) != 0u) { int distance = (event->pos() - m_startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) { dragged = performDrag(); } } if (!dragged) { QTreeView::mouseMoveEvent(event); } } void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) { QAbstractItemView::closeEditor(editor, hint); setEditing(false); } void MyTreeView::editorDestroyed(QObject *editor) { QAbstractItemView::editorDestroyed(editor); setEditing(false); } void MyTreeView::keyPressEvent(QKeyEvent *event) { if (isEditing()) { QTreeView::keyPressEvent(event); return; } QModelIndex currentIndex = this->currentIndex(); if (event->key() == Qt::Key_Return && currentIndex.isValid()) { if (this->isExpanded(currentIndex)) { this->collapse(currentIndex); } else { this->expand(currentIndex); } } QTreeView::keyPressEvent(event); } bool MyTreeView::isEditing() const { return state() == QAbstractItemView::EditingState; } void MyTreeView::setEditing(bool edit) { setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState); } bool MyTreeView::performDrag() { QModelIndexList bases = selectedIndexes(); QModelIndexList indexes; for (int i = 0; i < bases.count(); i++) { if (bases.at(i).column() == 0) { indexes << bases.at(i); } } if (indexes.isEmpty()) { return false; } auto *drag = new QDrag(this); drag->setMimeData(model()->mimeData(indexes)); QModelIndex ix = indexes.constFirst(); if (ix.isValid()) { QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value(); QPixmap pix = icon.pixmap(iconSize()); QSize size = pix.size(); QImage image(size, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter p(&image); p.setOpacity(0.7); p.drawPixmap(0, 0, pix); p.setOpacity(1); if (indexes.count() > 1) { QPalette palette; int radius = size.height() / 3; p.setBrush(palette.highlight()); p.setPen(palette.highlightedText().color()); p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius); p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count())); } p.end(); drag->setPixmap(QPixmap::fromImage(image)); } drag->exec(); return true; } BinMessageWidget::BinMessageWidget(QWidget *parent) : KMessageWidget(parent) { } BinMessageWidget::BinMessageWidget(const QString &text, QWidget *parent) : KMessageWidget(text, parent) { } bool BinMessageWidget::event(QEvent *ev) { if (ev->type() == QEvent::Hide || ev->type() == QEvent::Close) { emit messageClosing(); } return KMessageWidget::event(ev); } SmallJobLabel::SmallJobLabel(QWidget *parent) : QPushButton(parent) , m_action(nullptr) { setFixedWidth(0); setFlat(true); m_timeLine = new QTimeLine(500, this); QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged); QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished); hide(); } const QString SmallJobLabel::getStyleSheet(const QPalette &p) { KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window); QColor bg = scheme.background(KColorScheme::LinkBackground).color(); QColor fg = scheme.foreground(KColorScheme::LinkText).color(); QString style = QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue()); bg = scheme.background(KColorScheme::ActiveBackground).color(); fg = scheme.foreground(KColorScheme::ActiveText).color(); style.append( QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue())); return style; } void SmallJobLabel::setAction(QAction *action) { m_action = action; } void SmallJobLabel::slotTimeLineChanged(qreal value) { setFixedWidth(qMin(value * 2, qreal(1.0)) * sizeHint().width()); update(); } void SmallJobLabel::slotTimeLineFinished() { if (m_timeLine->direction() == QTimeLine::Forward) { // Show m_action->setVisible(true); } else { // Hide m_action->setVisible(false); setText(QString()); } } void SmallJobLabel::slotSetJobCount(int jobCount) { if (jobCount > 0) { // prepare animation setText(i18np("%1 job", "%1 jobs", jobCount)); setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount)); if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(sizeHint().width()); m_action->setVisible(true); return; } if (m_action->isVisible()) { setFixedWidth(sizeHint().width()); update(); return; } setFixedWidth(0); m_action->setVisible(true); int wantedWidth = sizeHint().width(); setGeometry(-wantedWidth, 0, wantedWidth, height()); m_timeLine->setDirection(QTimeLine::Forward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } else { if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(0); m_action->setVisible(false); return; } // hide m_timeLine->setDirection(QTimeLine::Backward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } } /** * @class BinItemDelegate * @brief This class is responsible for drawing items in the QTreeView. */ class BinItemDelegate : public QStyledItemDelegate { public: explicit BinItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() != 0) { return QStyledItemDelegate::updateEditorGeometry(editor, option, index); } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QRect r1 = option.rect; QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); double factor = (double)opt.decorationSize.height() / r1.height(); int decoWidth = 2 * textMargin; int mid = 0; if (factor > 0) { decoWidth += opt.decorationSize.width() / factor; } if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { mid = (int)((r1.height() / 2)); } r1.adjust(decoWidth, 0, 0, -mid); QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect(); editor->setGeometry(r2); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize hint = QStyledItemDelegate::sizeHint(option, index); QString text = index.data(AbstractProjectItem::DataName).toString(); QRectF r = option.rect; QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin; hint.setWidth(width); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if (type == AbstractProjectItem::FolderItem || type == AbstractProjectItem::FolderUpItem) { return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height())); } if (type == AbstractProjectItem::ClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); } if (type == AbstractProjectItem::SubClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5)))); } QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); QString line1 = index.data(Qt::DisplayRole).toString(); QString line2 = index.data(Qt::UserRole).toString(); int textW = qMax(option.fontMetrics.width(line1), option.fontMetrics.width(line2)); QSize iconSize = icon.actualSize(option.decorationSize); return QSize(qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4); } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0 && !index.data().isNull()) { QRect r1 = option.rect; painter->save(); painter->setClipRect(r1); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if ((option.state & static_cast((QStyle::State_Selected) != 0)) != 0) { painter->setPen(option.palette.highlightedText().color()); } else { painter->setPen(option.palette.text().color()); } QRect r = r1; QFont font = painter->font(); font.setBold(true); painter->setFont(font); if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { double factor = (double)opt.decorationSize.height() / r1.height(); int decoWidth = 2 * textMargin; if (factor > 0) { r.setWidth(opt.decorationSize.width() / factor); // Draw thumbnail opt.icon.paint(painter, r); decoWidth += r.width(); } int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(AbstractProjectItem::DataDuration).toString(); if (!subText.isEmpty()) { r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); // Draw usage counter int usage = index.data(AbstractProjectItem::UsageCount).toInt(); if (usage > 0) { bounding.moveLeft(bounding.right() + (2 * textMargin)); QString us = QString().sprintf("[%d]", usage); painter->drawText(bounding, Qt::AlignLeft | Qt::AlignTop, us, &bounding); } } if (type == AbstractProjectItem::ClipItem) { // Overlay icon if necessary QVariant v = index.data(AbstractProjectItem::IconOverlay); if (!v.isNull()) { QIcon reload = QIcon::fromTheme(v.toString()); r.setTop(r.bottom() - bounding.height()); r.setWidth(bounding.height()); reload.paint(painter, r); } int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt(); JobManagerStatus status = index.data(AbstractProjectItem::JobStatus).value(); if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) { // Draw job progress bar int progressWidth = option.fontMetrics.averageCharWidth() * 8; int progressHeight = option.fontMetrics.ascent() / 4; QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight); painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); if (status == JobManagerStatus::Running) { painter->drawRoundedRect(progress, 2, 2); painter->setBrush((option.state & static_cast((QStyle::State_Selected) != 0)) != 0 ? option.palette.text() : option.palette.highlight()); progress.setWidth((progressWidth - 2) * jobProgress / 100); painter->drawRoundedRect(progress, 2, 2); } else { // Draw kind of a pause icon progress.setWidth(3); painter->drawRect(progress); progress.moveLeft(progress.right() + 3); painter->drawRect(progress); } } QString jobText = index.data(AbstractProjectItem::JobMessage).toString(); if (!jobText.isEmpty()) { // QRectF txtBounding = painter->boundingRect(r2, Qt::AlignRight | Qt::AlignVCenter, " " + jobText + " "); painter->setPen(Qt::NoPen); painter->setBrush(option.palette.highlight()); // painter->drawRoundedRect(txtBounding, 2, 2); painter->setPen(option.palette.highlightedText().color()); painter->drawText(r2, Qt::AlignCenter, jobText); } } } else { // Folder or Folder Up items double factor = (double)opt.decorationSize.height() / r1.height(); int decoWidth = 2 * textMargin; if (factor > 0) { r.setWidth(opt.decorationSize.width() / factor); // Draw thumbnail opt.icon.paint(painter, r); decoWidth += r.width(); } r1.adjust(decoWidth, 0, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); } painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } }; LineEventEater::LineEventEater(QObject *parent) : QObject(parent) { } bool LineEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::ShortcutOverride: if (((QKeyEvent *)event)->key() == Qt::Key_Escape) { emit clearSearchLine(); } break; case QEvent::Resize: // Workaround Qt BUG 54676 emit showClearButton(((QResizeEvent *)event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8); break; default: break; } return QObject::eventFilter(obj, event); } Bin::Bin(const std::shared_ptr &model, QWidget *parent) : QWidget(parent) , isLoading(false) , m_itemModel(model) , m_itemView(nullptr) , m_doc(nullptr) , m_extractAudioAction(nullptr) , m_transcodeAction(nullptr) , m_clipsActionsMenu(nullptr) , m_inTimelineAction(nullptr) , m_listType((BinViewType)KdenliveSettings::binMode()) , m_iconSize(160, 90) , m_propertiesPanel(nullptr) , m_blankThumb() , m_invalidClipDialog(nullptr) , m_gainedFocus(false) , m_audioDuration(0) , m_processedAudio(0) { m_layout = new QVBoxLayout(this); // Create toolbar for buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_layout->addWidget(m_toolbar); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // Search line m_proxyModel = new ProjectSortProxyModel(this); m_proxyModel->setDynamicSortFilter(true); m_searchLine = new QLineEdit(this); m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); // m_searchLine->setClearButtonEnabled(true); m_searchLine->setPlaceholderText(i18n("Search")); m_searchLine->setFocusPolicy(Qt::ClickFocus); connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel, &ProjectSortProxyModel::slotSetSearchString); auto *leventEater = new LineEventEater(this); m_searchLine->installEventFilter(leventEater); connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear); connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton); setFocusPolicy(Qt::ClickFocus); connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel); connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs); connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip); connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers); connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage); // Connect models m_proxyModel->setSourceModel(m_itemModel.get()); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel, &ProjectSortProxyModel::slotDataChanged); connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); connect(m_itemModel.get(), static_cast(&ProjectItemModel::itemDropped), this, static_cast(&Bin::slotItemDropped)); connect(m_itemModel.get(), static_cast &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this, static_cast &, const QModelIndex &)>(&Bin::slotItemDropped)); connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited); connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel); // Zoom slider m_slider = new QSlider(Qt::Horizontal, this); m_slider->setMaximumWidth(100); m_slider->setMinimumWidth(40); m_slider->setRange(0, 10); m_slider->setValue(KdenliveSettings::bin_zoom()); connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize); auto *widgetslider = new QWidgetAction(this); widgetslider->setDefaultWidget(m_slider); // View type KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this); pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType); QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View")); listType->addAction(treeViewAction); treeViewAction->setData(BinTreeView); if (m_listType == treeViewAction->data().toInt()) { listType->setCurrentAction(treeViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction); QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View")); iconViewAction->setData(BinIconView); if (m_listType == iconViewAction->data().toInt()) { listType->setCurrentAction(iconViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction); QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this); connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); }); disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); disableEffects->setData("disable_bin_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects); #if KXMLGUI_VERSION_MINOR > 24 || KXMLGUI_VERSION_MAJOR > 5 m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), this); m_renameAction->setText(i18n("Rename")); #else m_renameAction = new QAction(i18n("Rename"), this); connect(m_renameAction, &QAction::triggered, this, &Bin::slotRenameItem); m_renameAction->setShortcut(Qt::Key_F2); #endif m_renameAction->setData("rename"); pCore->window()->actionCollection()->addAction(QStringLiteral("rename"), m_renameAction); listType->setToolBarMode(KSelectAction::MenuMode); connect(listType, static_cast(&KSelectAction::triggered), this, &Bin::slotInitView); // Settings menu QMenu *settingsMenu = new QMenu(i18n("Settings"), this); settingsMenu->addAction(listType); QMenu *sliderMenu = new QMenu(i18n("Zoom"), this); sliderMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); sliderMenu->addAction(widgetslider); settingsMenu->addMenu(sliderMenu); // Column show / hide actions m_showDate = new QAction(i18n("Show date"), this); m_showDate->setCheckable(true); connect(m_showDate, &QAction::triggered, this, &Bin::slotShowDateColumn); m_showDesc = new QAction(i18n("Show description"), this); m_showDesc->setCheckable(true); connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowDescColumn); settingsMenu->addAction(m_showDate); settingsMenu->addAction(m_showDesc); settingsMenu->addAction(disableEffects); auto *button = new QToolButton; button->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); button->setToolTip(i18n("Options")); button->setMenu(settingsMenu); button->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(button); // small info button for pending jobs m_infoLabel = new SmallJobLabel(this); m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette())); connect(pCore->jobManager().get(), &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount); QAction *infoAction = m_toolbar->addWidget(m_infoLabel); m_jobsMenu = new QMenu(this); // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu); m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this); m_cancelJobs->setCheckable(false); m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this); m_discardCurrentClipJobs->setCheckable(false); m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this); m_discardPendingJobs->setCheckable(false); m_jobsMenu->addAction(m_cancelJobs); m_jobsMenu->addAction(m_discardCurrentClipJobs); m_jobsMenu->addAction(m_discardPendingJobs); m_infoLabel->setMenu(m_jobsMenu); m_infoLabel->setAction(infoAction); // Hack, create toolbar spacer QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_toolbar->addWidget(spacer); // Add search line m_toolbar->addWidget(m_searchLine); m_binTreeViewDelegate = new BinItemDelegate(this); // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*))); m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1()); m_propertiesPanel = new QScrollArea(this); m_propertiesPanel->setFrameShape(QFrame::NoFrame); // Info widget for failed jobs, other errors m_infoMessage = new BinMessageWidget(this); m_layout->addWidget(m_infoMessage); m_infoMessage->setCloseButtonVisible(false); connect(m_infoMessage, &BinMessageWidget::messageClosing, this, &Bin::slotResetInfoMessage); // m_infoMessage->setWordWrap(true); m_infoMessage->hide(); connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval); connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType))); } Bin::~Bin() { blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); abortOperations(); m_itemModel->clean(); } QDockWidget *Bin::clipPropertiesDock() { return m_propertiesDock; } void Bin::abortOperations() { blockSignals(true); abortAudioThumbs(); if (m_propertiesPanel) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } } delete m_itemView; m_itemView = nullptr; blockSignals(false); } void Bin::abortAudioThumbs() { // TODO refac /* if (!m_audioThumbsThread.isRunning()) { return; } if (!m_processingAudioThumb.isEmpty()) { std::shared_ptr clip = m_itemModel->getClipByBinID(m_processingAudioThumb); if (clip) { clip->abortAudioThumbs(); } } m_audioThumbMutex.lock(); for (const QString &id : m_audioThumbsList) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0); } } m_audioThumbsList.clear(); m_audioThumbMutex.unlock(); m_audioThumbsThread.waitForFinished(); */ } void Bin::slotCreateAudioThumbs() { // TODO refac /* int max = m_audioThumbsList.count(); int count = 0; m_processedAudio = 0; while (!m_audioThumbsList.isEmpty()) { m_audioThumbMutex.lock(); max = qMax(max, m_audioThumbsList.count()); m_processingAudioThumb = m_audioThumbsList.takeFirst(); count++; m_audioThumbMutex.unlock(); std::shared_ptr clip = m_itemModel->getClipByBinID(m_processingAudioThumb); if (clip) { clip->slotCreateAudioThumbs(); m_processedAudio += (int)clip->duration().ms(); } else { qDebug() << "// Trying to create audio thumbs for unknown clip: " << m_processingAudioThumb; } } m_audioThumbMutex.lock(); m_processingAudioThumb.clear(); m_processedAudio = 0; m_audioDuration = 0; m_audioThumbMutex.unlock(); emitMessage(i18n("Audio thumbnails done"), 100, OperationCompletedMessage); */ } bool Bin::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::MouseButtonRelease) { if (!m_monitor->isActive()) { m_monitor->slotActivateMonitor(); } bool success = QWidget::eventFilter(obj, event); if (m_gainedFocus) { QMouseEvent *mouseEvent = static_cast(event); QAbstractItemView *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); m_gainedFocus = false; if (idx.isValid()) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); editMasterEffect(item); } else { editMasterEffect(nullptr); } } // make sure we discard the focus indicator m_gainedFocus = false; } return success; } if (event->type() == QEvent::MouseButtonDblClick) { QMouseEvent *mouseEvent = static_cast(event); QAbstractItemView *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); if (!idx.isValid()) { // User double clicked on empty area slotAddClip(); } else { slotItemDoubleClicked(idx, mouseEvent->pos()); } } else { qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!"; } return true; } if (event->type() == QEvent::Wheel) { QWheelEvent *e = static_cast(event); if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) { slotZoomView(e->delta() > 0); // emit zoomView(e->delta() > 0); return true; } } return QWidget::eventFilter(obj, event); } void Bin::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QMenu *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } } void Bin::slotSaveHeaders() { if ((m_itemView != nullptr) && m_listType == BinTreeView) { // save current treeview state (column width) QTreeView *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64()); } } void Bin::slotZoomView(bool zoomIn) { if (m_itemModel->rowCount() == 0) { // Don't zoom on empty bin return; } int progress = (zoomIn) ? 1 : -1; m_slider->setValue(m_slider->value() + progress); } Monitor *Bin::monitor() { return m_monitor; } const QStringList Bin::getFolderInfo(const QModelIndex &selectedIx) { QModelIndexList indexes; if (selectedIx.isValid()) { indexes << selectedIx; } else { indexes = m_proxyModel->selectionModel()->selectedIndexes(); } if (indexes.isEmpty()) { // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } QModelIndex ix = indexes.constFirst(); if (ix.isValid() && (m_proxyModel->selectionModel()->isSelected(ix) || selectedIx.isValid())) { return m_itemModel->getEnclosingFolderInfo(m_proxyModel->mapToSource(ix)); } // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } void Bin::slotAddClip() { // Check if we are in a folder QString parentFolder = getCurrentFolder(); ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel); } std::shared_ptr Bin::getFirstSelectedClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return std::shared_ptr(); } for (const QModelIndex &ix : indexes) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip) { return clip; } } } return nullptr; } void Bin::slotDeleteClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector> items; bool included = false; bool usedFolder = false; auto checkInclusion = [](bool accum, std::shared_ptr item) { return accum || std::static_pointer_cast(item)->isIncludedInTimeline(); }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (!item) { qDebug() << "Suspicious: item not found when trying to delete"; continue; } included = included || item->accumulate(false, checkInclusion); // Check if we are deleting non-empty folders: usedFolder = usedFolder || item->childCount() > 0; items.push_back(item); } if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from timeline")) != KMessageBox::Continue)) { return; } if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; for (const auto &item : items) { - m_itemModel->requestBinClipDeletion(item, undo, redo); + m_itemModel->requestBinClipDeletion(item, undo, redo, list); } + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Delete bin Clips")); } void Bin::slotReloadClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); auto currentItem = std::static_pointer_cast(item); if (currentItem) { emit openClip(std::shared_ptr()); if (currentItem->clipType() == ClipType::Playlist) { // Check if a clip inside playlist is missing QString path = currentItem->url(); QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile)); } } } } currentItem->reloadProducer(false); } } } void Bin::slotLocateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); auto currentItem = std::static_pointer_cast(item); if (currentItem) { QUrl url = QUrl::fromLocalFile(currentItem->url()).adjusted(QUrl::RemoveFilename); bool exists = QFile(url.toLocalFile()).exists(); if (currentItem->hasUrl() && exists) { QDesktopServices::openUrl(url); qCDebug(KDENLIVE_LOG) << " / / " + url.toString(); } else { if (!exists) { emitMessage(i18n("Couldn't locate ") + QString(" (" + url.toString() + QLatin1Char(')')), 100, ErrorMessage); } return; } } } } void Bin::slotDuplicateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); auto currentItem = std::static_pointer_cast(item); if (currentItem) { QDomDocument doc; QDomElement xml = currentItem->toXml(doc); if (!xml.isNull()) { QString currentName = EffectsList::property(xml, QStringLiteral("kdenlive:clipname")); if (currentName.isEmpty()) { QUrl url = QUrl::fromLocalFile(EffectsList::property(xml, QStringLiteral("resource"))); if (url.isValid()) { currentName = url.fileName(); } } if (!currentName.isEmpty()) { currentName.append(i18nc("append to clip name to indicate a copied idem", " (copy)")); EffectsList::setProperty(xml, QStringLiteral("kdenlive:clipname"), currentName); } QString id; m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip")); } } } } void Bin::setMonitor(Monitor *monitor) { m_monitor = monitor; connect(m_monitor, &Monitor::addClipToProject, this, &Bin::slotAddClipToProject); connect(m_monitor, &Monitor::refreshCurrentClip, this, &Bin::slotOpenCurrent); connect(this, &Bin::openClip, [&](std::shared_ptr clip, int in, int out) { m_monitor->slotOpenClip(clip, in, out); }); } void Bin::setDocument(KdenliveDoc *project) { m_infoMessage->hide(); blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); // Cleanup previous project m_itemModel->clean(); delete m_itemView; m_itemView = nullptr; m_doc = project; int iconHeight = QFontInfo(font()).pixelSize() * 3.5; m_iconSize = QSize(iconHeight * pCore->getCurrentDar(), iconHeight); setEnabled(true); blockSignals(false); m_proxyModel->selectionModel()->blockSignals(false); connect(m_proxyAction, SIGNAL(toggled(bool)), m_doc, SLOT(slotProxyCurrentItem(bool))); // connect(m_itemModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), m_itemView // connect(m_itemModel, SIGNAL(updateCurrentItem()), this, SLOT(autoSelect())); slotInitView(nullptr); bool binEffectsDisabled = getDocumentProperty(QStringLiteral("disablebineffects")).toInt() == 1; setBinEffectsEnabled(!binEffectsDisabled); } void Bin::createClip(const QDomElement &xml) { // Check if clip should be in a folder QString groupId = ProjectClip::getXmlProperty(xml, QStringLiteral("kdenlive:folderid")); std::shared_ptr parentFolder = m_itemModel->getFolderByBinId(groupId); if (!parentFolder) { parentFolder = m_itemModel->getRootFolder(); } QString path = EffectsList::property(xml, QStringLiteral("resource")); if (path.endsWith(QStringLiteral(".mlt")) || path.endsWith(QStringLiteral(".kdenlive"))) { QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile)); } } } } QString id = Xml::getTagContentByAttribute(xml, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id")); if (id.isEmpty()) { id = QString::number(m_itemModel->getFreeClipId()); } auto newClip = ProjectClip::construct(id, xml, m_blankThumb, m_itemModel); parentFolder->appendChild(newClip); } QString Bin::slotAddFolder(const QString &folderName) { auto parentFolder = m_itemModel->getFolderByBinId(getCurrentFolder()); qDebug() << "pranteforder id" << parentFolder->clipId(); QString newId; Fun undo = []() { return true; }; Fun redo = []() { return true; }; m_itemModel->requestAddFolder(newId, folderName.isEmpty() ? i18n("Folder") : folderName, parentFolder->clipId(), undo, redo); pCore->pushUndo(undo, redo, i18n("Create bin folder")); // Edit folder name if (!folderName.isEmpty()) { // We already have a name, no need to edit return newId; } auto folder = m_itemModel->getFolderByBinId(newId); auto ix = m_itemModel->getIndexFromItem(folder); qDebug() << "selecting" << ix; if (ix.isValid()) { qDebug() << "ix valid"; m_proxyModel->selectionModel()->clearSelection(); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->edit(m_proxyModel->mapFromSource(ix)); } return newId; } QModelIndex Bin::getIndexForId(const QString &id, bool folderWanted) const { QModelIndexList items = m_itemModel->match(m_itemModel->index(0, 0), AbstractProjectItem::DataId, QVariant::fromValue(id), 2, Qt::MatchRecursive); for (int i = 0; i < items.count(); i++) { AbstractProjectItem *currentItem = static_cast(items.at(i).internalPointer()); AbstractProjectItem::PROJECTITEMTYPE type = currentItem->itemType(); if (folderWanted && type == AbstractProjectItem::FolderItem) { // We found our folder return items.at(i); } if (!folderWanted && type == AbstractProjectItem::ClipItem) { // We found our clip return items.at(i); } } return QModelIndex(); } void Bin::selectClipById(const QString &clipId, int frame, const QPoint &zone) { if (m_monitor->activeClipId() == clipId) { if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } return; } m_proxyModel->selectionModel()->clearSelection(); std::shared_ptr clip = getBinClip(clipId); if (clip) { selectClip(clip); if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } } } void Bin::selectProxyModel(const QModelIndex &id) { if (isLoading) { // return; } if (id.isValid()) { if (id.column() != 0) { return; } std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(id)); if (currentItem) { // Set item as current so that it displays its content in clip monitor setCurrent(currentItem); if (currentItem->itemType() == AbstractProjectItem::ClipItem) { m_reloadAction->setEnabled(true); m_locateAction->setEnabled(true); m_duplicateAction->setEnabled(true); std::shared_ptr clip = std::static_pointer_cast(currentItem); ClipType::ProducerType type = clip->clipType(); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::Text || type == ClipType::TextTemplate); showClipProperties(clip, false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); emit findInTimeline(clip->clipId(), clip->timelineInstances()); } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) { // A folder was selected, disable editing clip m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Folder")); m_proxyAction->setText(i18n("Proxy Folder")); } else if (currentItem->itemType() == AbstractProjectItem::SubClipItem) { showClipProperties(std::static_pointer_cast(currentItem->parent()), false); m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); } m_deleteAction->setEnabled(true); } else { emit findInTimeline(QString()); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); } } else { // No item selected in bin m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); showClipProperties(nullptr); emit findInTimeline(QString()); emit requestClipShow(nullptr); // clear effect stack emit requestShowEffectStack(QString(), nullptr, QSize(), false); // Display black bg in clip monitor emit openClip(std::shared_ptr()); } } std::vector Bin::selectedClipsIds(bool excludeFolders) { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector ids; // We define the lambda that will be executed on each item of the subset of nodes of the tree that are selected auto itemAdder = [excludeFolders, &ids](std::vector &ids_vec, std::shared_ptr item) { auto binItem = std::static_pointer_cast(item); if (!excludeFolders || (binItem->itemType() != AbstractProjectItem::FolderItem && binItem->itemType() != AbstractProjectItem::FolderUpItem)) { ids.push_back(binItem->clipId()); } return ids_vec; }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); item->accumulate(ids, itemAdder); } return ids; } QList> Bin::selectedClips() { auto ids = selectedClipsIds(true); QList> ret; for (const auto &id : ids) { ret.push_back(m_itemModel->getClipByBinID(id)); } return ret; } void Bin::slotInitView(QAction *action) { if (action) { m_proxyModel->selectionModel()->clearSelection(); int viewType = action->data().toInt(); KdenliveSettings::setBinMode(viewType); if (viewType == m_listType) { return; } if (m_listType == BinTreeView) { // save current treeview state (column width) QTreeView *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); } else { // remove the current folderUp item if any if (m_folderUp) { if (m_folderUp->parent()) { m_folderUp->parent()->removeChild(m_folderUp); } m_folderUp.reset(); } } m_listType = static_cast(viewType); } if (m_itemView) { delete m_itemView; } switch (m_listType) { case BinIconView: m_itemView = new MyListView(this); m_folderUp = ProjectFolderUp::construct(m_itemModel); m_showDate->setEnabled(false); m_showDesc->setEnabled(false); break; default: m_itemView = new MyTreeView(this); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); break; } m_itemView->setMouseTracking(true); m_itemView->viewport()->installEventFilter(this); QSize zoom = m_iconSize * (m_slider->value() / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); m_itemView->setModel(m_proxyModel); m_itemView->setSelectionModel(m_proxyModel->selectionModel()); m_layout->insertWidget(1, m_itemView); // setup some default view specific parameters if (m_listType == BinTreeView) { m_itemView->setItemDelegate(m_binTreeViewDelegate); MyTreeView *view = static_cast(m_itemView); view->setSortingEnabled(true); view->setWordWrap(true); connect(m_proxyModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting); m_proxyModel->setDynamicSortFilter(true); if (!m_headerInfo.isEmpty()) { view->header()->restoreState(m_headerInfo); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); view->resizeColumnToContents(0); view->setColumnHidden(1, true); view->setColumnHidden(2, true); } m_showDate->setChecked(!view->isColumnHidden(1)); m_showDesc->setChecked(!view->isColumnHidden(2)); connect(view->header(), &QHeaderView::sectionResized, this, &Bin::slotSaveHeaders); connect(view->header(), &QHeaderView::sectionClicked, this, &Bin::slotSaveHeaders); connect(view, &MyTreeView::focusView, this, &Bin::slotGotFocus); } else if (m_listType == BinIconView) { MyListView *view = static_cast(m_itemView); connect(view, &MyListView::focusView, this, &Bin::slotGotFocus); } m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked); m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_itemView->setDragDropMode(QAbstractItemView::DragDrop); m_itemView->setAlternatingRowColors(true); m_itemView->setAcceptDrops(true); m_itemView->setFocus(); } void Bin::slotSetIconSize(int size) { if (!m_itemView) { return; } KdenliveSettings::setBin_zoom(size); QSize zoom = m_iconSize; zoom = zoom * (size / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); } void Bin::rebuildMenu() { m_transcodeAction = static_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); m_extractAudioAction = static_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); m_clipsActionsMenu = static_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); m_menu->insertMenu(m_reloadAction, m_extractAudioAction); m_menu->insertMenu(m_reloadAction, m_transcodeAction); m_menu->insertMenu(m_reloadAction, m_clipsActionsMenu); m_inTimelineAction = m_menu->insertMenu(m_reloadAction, static_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window()))); } void Bin::contextMenuEvent(QContextMenuEvent *event) { bool enableClipActions = false; ClipType::ProducerType type = ClipType::Unknown; bool isFolder = false; bool isImported = false; QString clipService; QString audioCodec; if (m_itemView) { QModelIndex idx = m_itemView->indexAt(m_itemView->viewport()->mapFromGlobal(event->globalPos())); if (idx.isValid()) { // User right clicked on a clip std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); if (currentItem) { enableClipActions = true; if (currentItem->itemType() == AbstractProjectItem::FolderItem) { isFolder = true; } else { auto clip = std::static_pointer_cast(currentItem); if (clip) { m_proxyAction->blockSignals(true); emit findInTimeline(clip->clipId(), clip->timelineInstances()); clipService = clip->getProducerProperty(QStringLiteral("mlt_service")); m_proxyAction->setChecked(clip->hasProxy()); QList transcodeActions; if (m_transcodeAction) { transcodeActions = m_transcodeAction->actions(); } QStringList dataList; QString condition; audioCodec = clip->codec(true); QString videoCodec = clip->codec(false); type = clip->clipType(); if (clip->hasUrl()) { isImported = true; } bool noCodecInfo = false; if (audioCodec.isEmpty() && videoCodec.isEmpty()) { noCodecInfo = true; } for (int i = 0; i < transcodeActions.count(); ++i) { dataList = transcodeActions.at(i)->data().toStringList(); if (dataList.count() > 4) { condition = dataList.at(4); if (condition.isEmpty()) { transcodeActions.at(i)->setEnabled(true); continue; } if (noCodecInfo) { // No audio / video codec, this is an MLT clip, disable conditionnal transcoding transcodeActions.at(i)->setEnabled(false); continue; } if (condition.startsWith(QLatin1String("vcodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == videoCodec); } else if (condition.startsWith(QLatin1String("acodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == audioCodec); } } } } m_proxyAction->blockSignals(false); } } } } // Enable / disable clip actions m_proxyAction->setEnabled((m_doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0) && enableClipActions); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::TextTemplate || type == ClipType::Text); m_reloadAction->setEnabled(enableClipActions); m_locateAction->setEnabled(enableClipActions); m_duplicateAction->setEnabled(enableClipActions); m_editAction->setVisible(!isFolder); m_clipsActionsMenu->setEnabled(enableClipActions); m_extractAudioAction->setEnabled(enableClipActions); m_openAction->setVisible(!isFolder); m_reloadAction->setVisible(!isFolder); m_duplicateAction->setVisible(!isFolder); m_inTimelineAction->setVisible(!isFolder); if (m_transcodeAction) { m_transcodeAction->setEnabled(enableClipActions); m_transcodeAction->menuAction()->setVisible(!isFolder && clipService.contains(QStringLiteral("avformat"))); } m_clipsActionsMenu->menuAction()->setVisible( !isFolder && (clipService.contains(QStringLiteral("avformat")) || clipService.contains(QStringLiteral("xml")) || clipService.contains(QStringLiteral("consumer")))); m_extractAudioAction->menuAction()->setVisible(!isFolder && !audioCodec.isEmpty()); m_locateAction->setVisible(!isFolder && (isImported)); // Show menu event->setAccepted(true); if (enableClipActions) { m_menu->exec(event->globalPos()); } else { // Clicked in empty area m_addButton->menu()->exec(event->globalPos()); } } void Bin::slotItemDoubleClicked(const QModelIndex &ix, const QPoint pos) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (m_listType == BinIconView) { if (item->childCount() > 0 || item->itemType() == AbstractProjectItem::FolderItem) { m_folderUp->changeParent(std::static_pointer_cast(item)); m_itemView->setRootIndex(ix); return; } if (item == m_folderUp) { std::shared_ptr parentItem = item->parent(); QModelIndex parent = getIndexForId(parentItem->parent()->clipId(), parentItem->parent()->itemType() == AbstractProjectItem::FolderItem); if (parentItem->parent() != m_itemModel->getRootFolder()) { // We are entering a parent folder m_folderUp->changeParent(std::static_pointer_cast(parentItem->parent())); } else { m_folderUp->changeParent(std::shared_ptr()); } m_itemView->setRootIndex(m_proxyModel->mapFromSource(parent)); return; } } else { if (item->childCount() > 0) { QTreeView *view = static_cast(m_itemView); view->setExpanded(ix, !view->isExpanded(ix)); return; } } if (ix.isValid()) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setSize(m_itemView->iconSize()); if (!pos.isNull() && ((ix.column() == 2 && item->itemType() == AbstractProjectItem::ClipItem) || !IconRect.contains(pos))) { // User clicked outside icon, trigger rename m_itemView->edit(ix); return; } if (item->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { if (clip->clipType() == ClipType::Text || clip->clipType() == ClipType::TextTemplate) { // m_propertiesPanel->setEnabled(false); showTitleWidget(clip); } else { slotSwitchClipProperties(clip); } } } } } void Bin::slotEditClip() { QString panelId = m_propertiesPanel->property("clipId").toString(); QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); if (item->clipId() != panelId) { // wrong clip return; } auto clip = std::static_pointer_cast(item); QString parentFolder = getCurrentFolder(); switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::SlideShow: showSlideshowWidget(clip); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); break; default: break; } } void Bin::slotSwitchClipProperties() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); auto clip = std::static_pointer_cast(item); if (clip) { slotSwitchClipProperties(clip); return; } } slotSwitchClipProperties(nullptr); } void Bin::slotSwitchClipProperties(std::shared_ptr clip) { if (clip == nullptr) { m_propertiesPanel->setEnabled(false); return; } if (clip->clipType() == ClipType::SlideShow) { m_propertiesPanel->setEnabled(false); showSlideshowWidget(clip); } else if (clip->clipType() == ClipType::QText) { m_propertiesPanel->setEnabled(false); QString parentFolder = getCurrentFolder(); ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); } else { m_propertiesPanel->setEnabled(true); showClipProperties(clip); m_propertiesDock->show(); m_propertiesDock->raise(); } // Check if properties panel is not tabbed under Bin // if (!pCore->window()->isTabbedWith(m_propertiesDock, QStringLiteral("project_bin"))) { } void Bin::doRefreshPanel(const QString &id) { std::shared_ptr currentItem = getFirstSelectedClip(); if ((currentItem != nullptr) && currentItem->AbstractProjectItem::clipId() == id) { showClipProperties(currentItem, true); } } void Bin::showClipProperties(std::shared_ptr clip, bool forceRefresh) { if ((clip == nullptr) || !clip->isReady()) { m_propertiesPanel->setEnabled(false); return; } m_propertiesPanel->setEnabled(true); QString panelId = m_propertiesPanel->property("clipId").toString(); if (!forceRefresh && panelId == clip->AbstractProjectItem::clipId()) { // the properties panel is already displaying current clip, do nothing return; } // Cleanup widget for new content for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", clip->AbstractProjectItem::clipId()); QVBoxLayout *lay = static_cast(m_propertiesPanel->layout()); if (lay == nullptr) { lay = new QVBoxLayout(m_propertiesPanel); m_propertiesPanel->setLayout(lay); } ClipPropertiesController *panel = clip->buildProperties(m_propertiesPanel); connect(this, &Bin::refreshTimeCode, panel, &ClipPropertiesController::slotRefreshTimeCode); connect(panel, &ClipPropertiesController::updateClipProperties, this, &Bin::slotEditClipCommand); connect(panel, &ClipPropertiesController::seekToFrame, m_monitor, static_cast(&Monitor::slotSeek)); connect(panel, &ClipPropertiesController::editClip, this, &Bin::slotEditClip); connect(panel, SIGNAL(editAnalysis(QString, QString, QString)), this, SLOT(slotAddClipExtraData(QString, QString, QString))); lay->addWidget(panel); } void Bin::slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps) { auto *command = new EditClipCommand(this, id, oldProps, newProps, true); m_doc->commandStack()->push(command); } void Bin::reloadClip(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(); } void Bin::reloadMonitorIfActive(const QString &id) { if (m_monitor->activeClipId() == id) { slotOpenCurrent(); } } QStringList Bin::getBinFolderClipIds(const QString &id) const { QStringList ids; std::shared_ptr folder = m_itemModel->getFolderByBinId(id); if (folder) { for (int i = 0; i < folder->childCount(); i++) { std::shared_ptr child = std::static_pointer_cast(folder->child(i)); if (child->itemType() == AbstractProjectItem::ClipItem) { ids << child->clipId(); } } } return ids; } std::shared_ptr Bin::getBinClip(const QString &id) { std::shared_ptr clip = nullptr; if (id.contains(QLatin1Char('_'))) { clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0)); } else if (!id.isEmpty()) { clip = m_itemModel->getClipByBinID(id); } return clip; } void Bin::setWaitingStatus(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void Bin::slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage) { Q_UNUSED(replace); std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } emit requesteInvalidRemoval(id, clip->url(), errorMessage); } - // TODO refac cleanup /* void Bin::slotProducerReady(const requestClipInfo &info, std::shared_ptr producer) { std::shared_ptr clip = m_itemModel->getClipByBinID(info.clipId); if (clip) { if ((producer == nullptr || clip->setProducer(producer, info.replaceProducer)) && !clip->hasProxy()) { if (producer) { pCore->binController()->replaceBinPlaylistClip(info.clipId, producer); } emit producerReady(info.clipId); // Check for file modifications ClipType t = clip->clipType(); if (t == AV || t == Audio || t == Image || t == Video || t == Playlist || t == TextTemplate) { m_fileWatcher.addFile(info.clipId, clip->url()); } if (m_doc->useProxy()) { if (t == AV || t == Video) { int width = clip->getProducerIntProperty(QStringLiteral("meta.media.width")); if (m_doc->autoGenerateProxy(width)) { // Start proxy m_doc->slotProxyCurrentItem(true, {clip}); } } else if (t == Playlist) { // always proxy playlists m_doc->slotProxyCurrentItem(true, {clip}); } else if (t == Image && m_doc->autoGenerateImageProxy(clip->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy m_doc->slotProxyCurrentItem(true, {clip}); } } else { emit producerReady(info.clipId); } QString currentClip = m_monitor->activeClipId(); if (currentClip.isEmpty()) { // No clip displayed in monitor, check if item is selected QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { // No clip selected, focus this new one selectClip(clip); } else { for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if ((item != nullptr) && item->clipId() == info.clipId) { // Item was selected, show it in monitor setCurrent(item); break; } } } } else if (currentClip == info.clipId) { setCurrent(clip); } } } else { // Clip not found, create it QString groupId = producer->get("kdenlive:folderid"); std::shared_ptr parentFolder; if (!groupId.isEmpty() && groupId != QLatin1String("-1")) { parentFolder = m_itemModel->getFolderByBinId(groupId); if (!parentFolder) { // parent folder does not exist, put in root folder parentFolder = m_itemModel->getRootFolder(); } if (groupId.toInt() >= m_folderCounter) { m_folderCounter = groupId.toInt() + 1; } } else { parentFolder = m_itemModel->getRootFolder(); } std::shared_ptr newClip = ProjectClip::construct(info.clipId, m_blankThumb, m_itemModel, producer); parentFolder->appendChild(newClip); emit producerReady(info.clipId); newClip->createAudioThumbs(); ClipType t = newClip->clipType(); if (t == AV || t == Audio || t == Image || t == Video || t == Playlist || t == TextTemplate) { m_fileWatcher.addFile(info.clipId, newClip->url()); } if (info.clipId.toInt() >= m_clipCounter) { m_clipCounter = info.clipId.toInt() + 1; } } } */ void Bin::selectClip(const std::shared_ptr &clip) { QModelIndex ix = m_itemModel->getIndexFromItem(clip); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix)); } void Bin::slotOpenCurrent() { std::shared_ptr currentItem = getFirstSelectedClip(); if (currentItem) { emit openClip(currentItem); } } void Bin::openProducer(std::shared_ptr controller) { emit openClip(controller); } void Bin::openProducer(std::shared_ptr controller, int in, int out) { emit openClip(controller, in, out); } void Bin::emitItemUpdated(std::shared_ptr item) { emit itemUpdated(item); } void Bin::emitRefreshPanel(const QString &id) { emit refreshPanel(id); } void Bin::setupGeneratorMenu() { if (!m_menu) { qCDebug(KDENLIVE_LOG) << "Warning, menu was not created, something is wrong"; return; } QMenu *addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window())); if (addMenu) { QMenu *menu = m_addButton->menu(); menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_addButton->setMenu(menu); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_extractAudioAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_transcodeAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_clipsActionsMenu = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window())); if (addMenu) { m_inTimelineAction = m_menu->addMenu(addMenu); m_inTimelineAction->setEnabled(!addMenu->isEmpty()); } if (m_locateAction) { m_menu->addAction(m_locateAction); } if (m_reloadAction) { m_menu->addAction(m_reloadAction); } if (m_duplicateAction) { m_menu->addAction(m_duplicateAction); } if (m_proxyAction) { m_menu->addAction(m_proxyAction); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_timeline"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(false); } m_menu->addAction(m_editAction); m_menu->addAction(m_openAction); m_menu->addAction(m_renameAction); m_menu->addAction(m_deleteAction); m_menu->insertSeparator(m_deleteAction); } void Bin::setupMenu(QMenu *addMenu, QAction *defaultAction, const QHash &actions) { // Setup actions QAction *first = m_toolbar->actions().at(0); m_deleteAction = actions.value(QStringLiteral("delete")); m_toolbar->insertAction(first, m_deleteAction); QAction *folder = actions.value(QStringLiteral("folder")); m_toolbar->insertAction(m_deleteAction, folder); m_editAction = actions.value(QStringLiteral("properties")); m_openAction = actions.value(QStringLiteral("open")); m_reloadAction = actions.value(QStringLiteral("reload")); m_duplicateAction = actions.value(QStringLiteral("duplicate")); m_locateAction = actions.value(QStringLiteral("locate")); m_proxyAction = actions.value(QStringLiteral("proxy")); auto *m = new QMenu(this); m->addActions(addMenu->actions()); m_addButton = new QToolButton(this); m_addButton->setMenu(m); m_addButton->setDefaultAction(defaultAction); m_addButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->insertWidget(folder, m_addButton); m_menu = new QMenu(this); m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel); m_propertiesDock->close(); // m_menu->addActions(addMenu->actions()); } const QString Bin::getDocumentProperty(const QString &key) { return m_doc->getDocumentProperty(key); } void Bin::slotUpdateJobStatus(const QString &id, int jobType, int status, const QString &label, const QString &actionName, const QString &details) { Q_UNUSED(id) Q_UNUSED(jobType) Q_UNUSED(status) Q_UNUSED(label) Q_UNUSED(actionName) Q_UNUSED(details) // TODO refac /* std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setJobStatus((AbstractClipJob::JOBTYPE)jobType, (ClipJobStatus)status); } if (status == JobCrashed) { QList actions = m_infoMessage->actions(); if (m_infoMessage->isHidden()) { if (!details.isEmpty()) { m_infoMessage->setText(label + QStringLiteral(" ") + i18n("Show log") + QStringLiteral("")); } else { m_infoMessage->setText(label); } m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); } if (!actionName.isEmpty()) { QAction *action = nullptr; QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); action = coll->action(actionName); if (action) { break; } } if ((action != nullptr) && !actions.contains(action)) { m_infoMessage->addAction(action); } } if (!details.isEmpty()) { m_errorLog.append(details); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); } */ } void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions) { // Remove axisting actions if any QList acts = m_infoMessage->actions(); while (!acts.isEmpty()) { QAction *a = acts.takeFirst(); m_infoMessage->removeAction(a); delete a; } m_infoMessage->setText(text); m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); for (QAction *action : actions) { m_infoMessage->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotMessageActionTriggered); } m_infoMessage->setCloseButtonVisible(actions.isEmpty()); m_infoMessage->setMessageType(type); if (m_infoMessage->isHidden()) { m_infoMessage->animatedShow(); } } void Bin::refreshClip(const QString &id) { if (m_monitor->activeClipId() == id) { m_monitor->refreshMonitorIfActive(); } } void Bin::doRefreshAudioThumbs(const QString &id) { if (m_monitor->activeClipId() == id) { slotSendAudioThumb(id); } } void Bin::slotCreateProjectClip() { QAction *act = qobject_cast(sender()); if (act == nullptr) { // Cannot access triggering action, something is wrong qCDebug(KDENLIVE_LOG) << "// Error in clip creation action"; return; } ClipType::ProducerType type = (ClipType::ProducerType)act->data().toInt(); QStringList folderInfo = getFolderInfo(); QString parentFolder = getCurrentFolder(); switch (type) { case ClipType::Color: ClipCreationDialog::createColorClip(m_doc, parentFolder, m_itemModel); break; case ClipType::SlideShow: ClipCreationDialog::createSlideshowClip(m_doc, parentFolder, m_itemModel); break; case ClipType::Text: ClipCreationDialog::createTitleClip(m_doc, parentFolder, QString(), m_itemModel); break; case ClipType::TextTemplate: ClipCreationDialog::createTitleTemplateClip(m_doc, parentFolder, m_itemModel); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this); break; default: break; } } void Bin::slotItemDropped(const QStringList &ids, const QModelIndex &parent) { std::shared_ptr parentItem; if (parent.isValid()) { parentItem = m_itemModel->getBinItemByIndex(parent); parentItem = parentItem->getEnclosingFolder(false); } else { parentItem = m_itemModel->getRootFolder(); } auto *moveCommand = new QUndoCommand(); moveCommand->setText(i18np("Move Clip", "Move Clips", ids.count())); QStringList folderIds; for (const QString &id : ids) { if (id.contains(QLatin1Char('/'))) { // trying to move clip zone, not allowed. Ignore continue; } if (id.startsWith(QLatin1Char('#'))) { // moving a folder, keep it for later folderIds << id; continue; } std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinClipCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } if (!folderIds.isEmpty()) { for (QString id : folderIds) { id.remove(0, 1); std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinFolderCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } } if (moveCommand->childCount() == 0) { pCore->displayMessage(i18n("No valid clip to insert"), InformationMessage, 500); } else { m_doc->commandStack()->push(moveCommand); } } void Bin::slotAddEffect(QString id, const QStringList &effectData) { if (id.isEmpty()) { id = m_monitor->activeClipId(); } if (!id.isEmpty()) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); clip->copyEffect(sourceStack, effectData.at(3).toInt()); } else { clip->addEffect(effectData.constFirst()); } return; } } pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } void Bin::slotEffectDropped(const QStringList &effectData, const QModelIndex &parent) { if (parent.isValid()) { std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); if (parentItem->itemType() != AbstractProjectItem::ClipItem) { // effect only supported on clip items return; } m_proxyModel->selectionModel()->clearSelection(); int row = parent.row(); const QModelIndex id = m_itemModel->index(row, 0, parent.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, parent.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } setCurrent(parentItem); if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); std::static_pointer_cast(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt()); } else { std::static_pointer_cast(parentItem)->addEffect(effectData.constFirst()); } } } void Bin::editMasterEffect(std::shared_ptr clip) { if (m_gainedFocus) { // Widget just gained focus, updating stack is managed in the eventfilter event, not from item return; } if (clip) { if (clip->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clp = std::static_pointer_cast(clip); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); return; } if (clip->itemType() == AbstractProjectItem::SubClipItem) { if (auto ptr = clip->parentItem().lock()) { std::shared_ptr clp = std::static_pointer_cast(ptr); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); } return; } } emit requestShowEffectStack(QString(), nullptr, QSize(), false); } void Bin::slotGotFocus() { m_gainedFocus = true; } void Bin::doMoveClip(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentItem->changeParent(newParent); } void Bin::doMoveFolder(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentParent->removeChild(currentItem); currentItem->changeParent(newParent); emit storeFolder(id, newParent->clipId(), currentParent->clipId(), currentItem->name()); } void Bin::droppedUrls(const QList &urls, const QStringList &folderInfo) { QModelIndex current; if (folderInfo.isEmpty()) { current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); } else { // get index for folder current = getIndexForId(folderInfo.constFirst(), true); } slotItemDropped(urls, current); } void Bin::slotAddClipToProject(const QUrl &url) { QList urls; urls << url; QModelIndex current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); slotItemDropped(urls, current); } void Bin::slotItemDropped(const QList &urls, const QModelIndex &parent) { QString parentFolder = m_itemModel->getRootFolder()->clipId(); if (parent.isValid()) { // Check if drop occured on a folder std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); while (parentItem->itemType() != AbstractProjectItem::FolderItem) { parentItem = parentItem->parent(); } parentFolder = parentItem->clipId(); } ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel); } void Bin::slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command) { Q_UNUSED(info) Q_UNUSED(url) Q_UNUSED(command) // TODO reimplement this /* // Create folder to hold imported clips QString folderName = QFileInfo(url).fileName().section(QLatin1Char('.'), 0, 0); QString folderId = QString::number(getFreeFolderId()); new AddBinFolderCommand(this, folderId, folderName.isEmpty() ? i18n("Folder") : folderName, m_itemModel->getRootFolder()->clipId(), false, command); // Parse playlist clips QDomDocument doc; QFile file(url); doc.setContent(&file, false); file.close(); bool invalid = false; if (doc.documentElement().isNull()) { invalid = true; } QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer")); QDomNodeList tracks = doc.documentElement().elementsByTagName(QStringLiteral("track")); if (invalid || producers.isEmpty()) { doDisplayMessage(i18n("Playlist clip %1 is invalid.", QFileInfo(url).fileName()), KMessageWidget::Warning); delete command; return; } if (tracks.count() > pCore->projectManager()->currentTimeline()->visibleTracksCount() + 1) { doDisplayMessage( i18n("Playlist clip %1 has too many tracks (%2) to be imported. Add new tracks to your project.", QFileInfo(url).fileName(), tracks.count()), KMessageWidget::Warning); delete command; return; } // Maps playlist producer IDs to (project) bin producer IDs. QMap idMap; // Maps hash IDs to (project) first playlist producer instance ID. This is // necessary to detect duplicate producer serializations produced by MLT. // This covers, for instance, images and titles. QMap hashToIdMap; QDir mltRoot(doc.documentElement().attribute(QStringLiteral("root"))); for (int i = 0; i < producers.count(); i++) { QDomElement prod = producers.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("id")); // track producer if (originalId.contains(QLatin1Char('_'))) { originalId = originalId.section(QLatin1Char('_'), 0, 0); } // slowmotion producer if (originalId.contains(QLatin1Char(':'))) { originalId = originalId.section(QLatin1Char(':'), 1, 1); } // We already have seen and mapped this producer. if (idMap.contains(originalId)) { continue; } // Check for duplicate producers, based on hash value of producer. // Be careful as to the kdenlive:file_hash! It is not unique for // title clips, nor color clips. Also not sure about image sequences. // So we use mlt service-specific hashes to identify duplicate producers. QString hash; QString mltService = EffectsList::property(prod, QStringLiteral("mlt_service")); if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("kdenlivetitle") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash = mltService + QLatin1Char(':') + EffectsList::property(prod, QStringLiteral("kdenlive:clipname")) + QLatin1Char(':') + EffectsList::property(prod, QStringLiteral("kdenlive:folderid")) + QLatin1Char(':'); if (mltService == QLatin1String("kdenlivetitle")) { // Calculate hash based on title contents. hash.append( QString(QCryptographicHash::hash(EffectsList::property(prod, QStringLiteral("xmldata")).toUtf8(), QCryptographicHash::Md5).toHex())); } else if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash.append(EffectsList::property(prod, QStringLiteral("resource"))); } QString singletonId = hashToIdMap.value(hash, QString()); if (singletonId.length() != 0) { // map duplicate producer ID to single bin clip producer ID. qCDebug(KDENLIVE_LOG) << "found duplicate producer:" << hash << ", reusing newID:" << singletonId; idMap.insert(originalId, singletonId); continue; } } // First occurence of a producer, so allocate new bin clip producer ID. QString newId = QString::number(getFreeClipId()); idMap.insert(originalId, newId); qCDebug(KDENLIVE_LOG) << "originalId: " << originalId << ", newId: " << newId; // Ensure to register new bin clip producer ID in hash hashmap for // those clips that MLT likes to serialize multiple times. This is // indicated by having a hash "value" unqual "". See also above. if (hash.length() != 0) { hashToIdMap.insert(hash, newId); } // Add clip QDomElement clone = prod.cloneNode(true).toElement(); EffectsList::setProperty(clone, QStringLiteral("kdenlive:folderid"), folderId); // Do we have a producer that uses a resource property that contains a path? if (mltService == QLatin1String("avformat-novalidate") // av clip || mltService == QLatin1String("avformat") // av clip || mltService == QLatin1String("pixbuf") // image (sequence) clip || mltService == QLatin1String("qimage") // image (sequence) clip || mltService == QLatin1String("xml") // MLT playlist clip, someone likes recursion :) ) { // Make sure to correctly resolve relative resource paths based on // the playlist's root, not on this project's root QString resource = EffectsList::property(clone, QStringLiteral("resource")); if (QFileInfo(resource).isRelative()) { QFileInfo rootedResource(mltRoot, resource); qCDebug(KDENLIVE_LOG) << "fixed resource path for producer, newId:" << newId << "resource:" << rootedResource.absoluteFilePath(); EffectsList::setProperty(clone, QStringLiteral("resource"), rootedResource.absoluteFilePath()); } } ClipCreationDialog::createClipsCommand(this, clone, newId, command); } pCore->projectManager()->currentTimeline()->importPlaylist(info, idMap, doc, command); */ } void Bin::slotItemEdited(const QModelIndex &ix, const QModelIndex &, const QVector &) { if (ix.isValid()) { // Clip renamed std::shared_ptr item = m_itemModel->getBinItemByIndex(ix); auto clip = std::static_pointer_cast(item); if (clip) { emit clipNameChanged(clip->AbstractProjectItem::clipId()); } } } void Bin::renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out) { auto *command = new RenameBinSubClipCommand(this, id, newName, oldName, in, out); m_doc->commandStack()->push(command); } void Bin::renameSubClip(const QString &id, const QString &newName, const QString &oldName, int in, int out) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } std::shared_ptr sub = clip->getSubClip(in, out); if (!sub) { return; } sub->setName(newName); clip->setProducerProperty("kdenlive:clipzone." + oldName, QString()); clip->setProducerProperty("kdenlive:clipzone." + newName, QString::number(in) + QLatin1Char(';') + QString::number(out)); emit itemUpdated(sub); } Timecode Bin::projectTimecode() const { return m_doc->timecode(); } void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams) { Q_UNUSED(info) Q_UNUSED(id) Q_UNUSED(filterParams) Q_UNUSED(consumerParams) Q_UNUSED(extraParams) // TODO refac /* std::shared_ptr clip = getBinClip(id); if (!clip) { return; } QMap producerParams = QMap(); producerParams.insert(QStringLiteral("producer"), clip->url()); if (info.cropDuration != GenTime()) { producerParams.insert(QStringLiteral("in"), QString::number((int)info.cropStart.frames(pCore->getCurrentFps()))); producerParams.insert(QStringLiteral("out"), QString::number((int)(info.cropStart + info.cropDuration).frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipStartPos"), QString::number((int)info.startPos.frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipTrack"), QString::number(info.track)); } else { // We want to process whole clip producerParams.insert(QStringLiteral("in"), QString::number(0)); producerParams.insert(QStringLiteral("out"), QString::number(-1)); } */ } void Bin::focusBinView() const { m_itemView->setFocus(); } void Bin::slotOpenClip() { std::shared_ptr clip = getFirstSelectedClip(); if (!clip) { return; } switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::Image: if (KdenliveSettings::defaultimageapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open images in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultimageapp(), QStringList() << clip->url()); } break; case ClipType::Audio: if (KdenliveSettings::defaultaudioapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open audio files in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultaudioapp(), QStringList() << clip->url()); } break; default: break; } } void Bin::updateTimecodeFormat() { emit refreshTimeCode(); } /* void Bin::slotGotFilterJobResults(const QString &id, int startPos, int track, const stringMap &results, const stringMap &filterInfo) { if (filterInfo.contains(QStringLiteral("finalfilter"))) { if (filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data std::shared_ptr clip = getBinClip(id); if (clip) { QString key = filterInfo.value(QStringLiteral("key")); QStringList newValue = clip->updatedAnalysisData(key, results.value(key), filterInfo.value(QStringLiteral("offset")).toInt()); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } if (startPos == -1) { // Processing bin clip std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr ctl = std::static_pointer_cast(currentItem); EffectsList list = ctl->effectList(); QDomElement effect = list.effectById(filterInfo.value(QStringLiteral("finalfilter"))); QDomDocument doc; QDomElement e = doc.createElement(QStringLiteral("test")); doc.appendChild(e); e.appendChild(doc.importNode(effect, true)); if (!effect.isNull()) { QDomElement newEffect = effect.cloneNode().toElement(); QMap::const_iterator i = results.constBegin(); while (i != results.constEnd()) { EffectsList::setParameter(newEffect, i.key(), i.value()); ++i; } ctl->updateEffect(newEffect, effect.attribute(QStringLiteral("kdenlive_ix")).toInt()); emit requestClipShow(currentItem); // TODO use undo / redo for bin clip edit effect // EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(), true, true); m_commandStack->push(command); emit clipItemSelected(clip); } // emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // This is a timeline filter, forward results emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // Currently, only the first value of results is used std::shared_ptr clip = getBinClip(id); if (!clip) { return; } // Check for return value int markersType = -1; if (filterInfo.contains(QStringLiteral("addmarkers"))) { markersType = filterInfo.value(QStringLiteral("addmarkers")).toInt(); } if (results.isEmpty()) { emit displayBinMessage(i18n("No data returned from clip analysis"), KMessageWidget::Warning); return; } bool dataProcessed = false; QString label = filterInfo.value(QStringLiteral("label")); QString key = filterInfo.value(QStringLiteral("key")); int offset = filterInfo.value(QStringLiteral("offset")).toInt(); QStringList value = results.value(key).split(QLatin1Char(';'), QString::SkipEmptyParts); // qCDebug(KDENLIVE_LOG)<<"// RESULT; "<setText(i18n("Auto Split Clip")); for (const QString &pos : value) { if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } new AddBinClipCutCommand(this, id, cutPos + offset, newPos + offset, true, command); cutPos = newPos; } if (command->childCount() == 0) { delete command; } else { m_doc->commandStack()->push(command); } } if (markersType >= 0) { // Add markers from returned data dataProcessed = true; int cutPos = 0; int index = 1; bool simpleList = false; double sourceFps = clip->getOriginalFps(); if (qFuzzyIsNull(sourceFps)) { sourceFps = pCore->getCurrentFps(); } if (filterInfo.contains(QStringLiteral("simplelist"))) { // simple list simpleList = true; } for (const QString &pos : value) { if (simpleList) { clip->getMarkerModel()->addMarker(GenTime((int)(pos.toInt() * pCore->getCurrentFps() / sourceFps), pCore->getCurrentFps()), label + pos, markersType); index++; continue; } if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } clip->getMarkerModel()->addMarker(GenTime(newPos + offset, pCore->getCurrentFps()), label + QString::number(index), markersType); index++; cutPos = newPos; } } if (!dataProcessed || filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data QStringList newValue = clip->updatedAnalysisData(key, results.value(key), offset); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } */ void Bin::slotGetCurrentProjectImage(const QString &clipId, bool request) { Q_UNUSED(clipId) // TODO refact : look at this // if (!clipId.isEmpty()) { // (pCore->projectManager()->currentTimeline()->hideClip(clipId, request)); // } pCore->monitorManager()->projectMonitor()->slotGetCurrentImage(request); } // TODO: move title editing into a better place... void Bin::showTitleWidget(std::shared_ptr clip) { QString path = clip->getProducerProperty(QStringLiteral("resource")); QDir titleFolder(m_doc->projectDataFolder() + QStringLiteral("/titles")); titleFolder.mkpath(QStringLiteral(".")); TitleWidget dia_ui(QUrl(), m_doc->timecode(), titleFolder.absolutePath(), pCore->monitorManager()->projectMonitor(), pCore->window()); connect(&dia_ui, &TitleWidget::requestBackgroundFrame, this, &Bin::slotGetCurrentProjectImage); QDomDocument doc; QString xmldata = clip->getProducerProperty(QStringLiteral("xmldata")); if (xmldata.isEmpty() && QFile::exists(path)) { QFile file(path); doc.setContent(&file, false); file.close(); } else { doc.setContent(xmldata); } dia_ui.setXml(doc, clip->AbstractProjectItem::clipId()); if (dia_ui.exec() == QDialog::Accepted) { QMap newprops; newprops.insert(QStringLiteral("xmldata"), dia_ui.xml().toString()); if (dia_ui.duration() != clip->duration().frames(pCore->getCurrentFps()) + 1) { // duration changed, we need to update duration newprops.insert(QStringLiteral("out"), QString::number(dia_ui.duration() - 1)); int currentLength = clip->getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (currentLength != dia_ui.duration()) { newprops.insert(QStringLiteral("kdenlive:duration"), QString::number(dia_ui.duration())); } } // trigger producer reload newprops.insert(QStringLiteral("force_reload"), QStringLiteral("2")); if (!path.isEmpty()) { // we are editing an external file, asked if we want to detach from that file or save the result to that title file. if (KMessageBox::questionYesNo(pCore->window(), i18n("You are editing an external title clip (%1). Do you want to save your changes to the title " "file or save the changes for this project only?", path), i18n("Save Title"), KGuiItem(i18n("Save to title file")), KGuiItem(i18n("Save in project only"))) == KMessageBox::Yes) { // save to external file dia_ui.saveTitle(QUrl::fromLocalFile(path)); } else { newprops.insert(QStringLiteral("resource"), QString()); } } slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops); } } void Bin::slotResetInfoMessage() { m_errorLog.clear(); QList actions = m_infoMessage->actions(); for (int i = 0; i < actions.count(); ++i) { m_infoMessage->removeAction(actions.at(i)); } } void Bin::emitMessage(const QString &text, int progress, MessageType type) { emit displayMessage(text, progress, type); } void Bin::slotSetSorting() { QTreeView *view = qobject_cast(m_itemView); if (view) { int ix = view->header()->sortIndicatorSection(); m_proxyModel->setFilterKeyColumn(ix); } } void Bin::slotShowDateColumn(bool show) { QTreeView *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(1, !show); } } void Bin::slotShowDescColumn(bool show) { QTreeView *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(2, !show); } } void Bin::slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage) { if (m_invalidClipDialog) { if (!url.isEmpty()) { m_invalidClipDialog->addClip(id, url); } return; } QString message = i18n("Clip is invalid, will be removed from project."); if (!errorMessage.isEmpty()) { message.append("\n" + errorMessage); } m_invalidClipDialog = new InvalidDialog(i18n("Invalid clip"), message, true, this); m_invalidClipDialog->addClip(id, url); int result = m_invalidClipDialog->exec(); if (result == QDialog::Accepted) { const QStringList ids = m_invalidClipDialog->getIds(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; for (const QString &i : ids) { auto item = m_itemModel->getClipByBinID(i); - m_itemModel->requestBinClipDeletion(item, undo, redo); + m_itemModel->requestBinClipDeletion(item, undo, redo, list); } + ModelUpdater::applyUpdates(undo, redo, list); } delete m_invalidClipDialog; m_invalidClipDialog = nullptr; } void Bin::slotRefreshClipThumbnail(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(true); } void Bin::slotAddClipExtraData(const QString &id, const QString &key, const QString &clipData, QUndoCommand *groupCommand) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } QString oldValue = clip->getProducerProperty(key); QMap oldProps; oldProps.insert(key, oldValue); QMap newProps; newProps.insert(key, clipData); auto *command = new EditClipCommand(this, id, oldProps, newProps, true, groupCommand); if (!groupCommand) { m_doc->commandStack()->push(command); } } void Bin::slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setProperties(properties, refreshPropertiesPanel); } } void Bin::updateTimelineProducers(const QString &id, const QMap &passProperties) { Q_UNUSED(id) Q_UNUSED(passProperties) // TODO REFAC // pCore->projectManager()->currentTimeline()->updateClipProperties(id, passProperties); // m_doc->renderer()->updateSlowMotionProducers(id, passProperties); } void Bin::showSlideshowWidget(std::shared_ptr clip) { QString folder = QFileInfo(clip->url()).absolutePath(); qCDebug(KDENLIVE_LOG) << " ** * CLIP ABS PATH: " << clip->url() << " = " << folder; SlideshowClip *dia = new SlideshowClip(m_doc->timecode(), folder, clip.get(), this); if (dia->exec() == QDialog::Accepted) { // edit clip properties QMap properties; properties.insert(QStringLiteral("out"), QString::number(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1)); properties.insert(QStringLiteral("kdenlive:duration"), QString::number(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount())); properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName()); properties.insert(QStringLiteral("ttl"), QString::number(m_doc->getFramePos(dia->clipDuration()))); properties.insert(QStringLiteral("loop"), QString::number(static_cast(dia->loop()))); properties.insert(QStringLiteral("crop"), QString::number(static_cast(dia->crop()))); properties.insert(QStringLiteral("fade"), QString::number(static_cast(dia->fade()))); properties.insert(QStringLiteral("luma_duration"), QString::number(m_doc->getFramePos(dia->lumaDuration()))); properties.insert(QStringLiteral("luma_file"), dia->lumaFile()); properties.insert(QStringLiteral("softness"), QString::number(dia->softness())); properties.insert(QStringLiteral("animation"), dia->animation()); QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("kdenlive:duration"), clip->getProducerProperty(QStringLiteral("kdenlive:duration"))); oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name()); oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl"))); oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop"))); oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop"))); oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade"))); oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration"))); oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file"))); oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness"))); oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation"))); slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } delete dia; } void Bin::setBinEffectsEnabled(bool enabled) { QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_bin_effects")); if (disableEffects) { if (enabled == disableEffects->isChecked()) { return; } disableEffects->blockSignals(true); disableEffects->setChecked(!enabled); disableEffects->blockSignals(false); } m_itemModel->setBinEffectsEnabled(enabled); pCore->projectManager()->disableBinEffects(!enabled); } void Bin::slotRenameItem() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedRows(0); for (const QModelIndex &ix : indexes) { if (!ix.isValid()) { continue; } m_itemView->setCurrentIndex(ix); m_itemView->edit(ix); return; } } void Bin::refreshProxySettings() { QList> clipList = m_itemModel->getRootFolder()->childClips(); auto *masterCommand = new QUndoCommand(); masterCommand->setText(m_doc->useProxy() ? i18n("Enable proxies") : i18n("Disable proxies")); if (!m_doc->useProxy()) { // Disable all proxies m_doc->slotProxyCurrentItem(false, clipList, false, masterCommand); } else { QList> toProxy; for (std::shared_ptr clp : clipList) { ClipType::ProducerType t = clp->clipType(); if (t == ClipType::Playlist) { toProxy << clp; continue; } else if ((t == ClipType::AV || t == ClipType::Video) && m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } else if (t == ClipType::Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } } if (!toProxy.isEmpty()) { m_doc->slotProxyCurrentItem(true, toProxy, false, masterCommand); } } if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } QStringList Bin::getProxyHashList() { QStringList list; QList> clipList = m_itemModel->getRootFolder()->childClips(); for (std::shared_ptr clp : clipList) { if (clp->clipType() == ClipType::AV || clp->clipType() == ClipType::Video || clp->clipType() == ClipType::Playlist) { list << clp->hash(); } } return list; } void Bin::slotSendAudioThumb(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if ((clip != nullptr) && clip->audioThumbCreated()) { m_monitor->prepareAudioThumb(clip->audioChannels(), clip->audioFrameCache); } else { QVariantList list; m_monitor->prepareAudioThumb(0, list); } } bool Bin::isEmpty() const { // TODO: return true if we only have folders if (m_itemModel->getRootFolder() == nullptr) { return true; } return m_itemModel->getRootFolder()->childCount() == 0; } void Bin::reloadAllProducers() { if (m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) { return; } QList> clipList = m_itemModel->getRootFolder()->childClips(); emit openClip(std::shared_ptr()); for (std::shared_ptr clip : clipList) { QDomDocument doc; QDomElement xml = clip->toXml(doc); // Make sure we reload clip length xml.removeAttribute(QStringLiteral("out")); EffectsList::removeProperty(xml, QStringLiteral("length")); if (!xml.isNull()) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); clip->discardAudioThumb(); // We need to set a temporary id before all outdated producers are replaced; pCore->jobManager()->startJob({clip->AbstractProjectItem::clipId()}, -1, QString(), xml); } } } void Bin::slotMessageActionTriggered() { m_infoMessage->animatedHide(); } void Bin::resetUsageCount() { const QList> clipList = m_itemModel->getRootFolder()->childClips(); for (std::shared_ptr clip : clipList) { clip->setRefCount(0); } } void Bin::getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize) { QList> clipList = m_itemModel->getRootFolder()->childClips(); for (std::shared_ptr clip : clipList) { if (clip->refCount() == 0) { *unused += 1; *unusedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } else { *used += 1; *usedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } } } QDir Bin::getCacheDir(CacheType type, bool *ok) const { return m_doc->getCacheDir(type, ok); } void Bin::rebuildProxies() { QList> clipList = m_itemModel->getRootFolder()->childClips(); QList> toProxy; for (std::shared_ptr clp : clipList) { if (clp->hasProxy()) { toProxy << clp; } } if (toProxy.isEmpty()) { return; } auto *masterCommand = new QUndoCommand(); masterCommand->setText(i18n("Rebuild proxies")); m_doc->slotProxyCurrentItem(true, toProxy, true, masterCommand); if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } void Bin::showClearButton(bool show) { m_searchLine->setClearButtonEnabled(show); } void Bin::saveZone(const QStringList &info, const QDir &dir) { if (info.size() != 3) { return; } std::shared_ptr clip = getBinClip(info.constFirst()); if (clip) { QPoint zone(info.at(1).toInt(), info.at(2).toInt()); clip->saveZone(zone, dir); } } void Bin::setCurrent(std::shared_ptr item) { switch (item->itemType()) { case AbstractProjectItem::ClipItem: { openProducer(std::static_pointer_cast(item)); std::shared_ptr clp = std::static_pointer_cast(item); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); break; } case AbstractProjectItem::SubClipItem: { auto subClip = std::static_pointer_cast(item); QPoint zone = subClip->zone(); openProducer(subClip->getMasterClip(), zone.x(), zone.y()); break; } case AbstractProjectItem::FolderUpItem: case AbstractProjectItem::FolderItem: default: break; } } void Bin::cleanup() { m_itemModel->requestCleanup(); } std::shared_ptr Bin::getClipEffectStack(int itemId) { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); std::shared_ptr effectStack = std::static_pointer_cast(clip)->m_effectStack; return effectStack; } int Bin::getClipDuration(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); return clip->frameDuration(); } QString Bin::getCurrentFolder() { // Check parent item QModelIndex ix = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr parentFolder = m_itemModel->getRootFolder(); if (ix.isValid() && m_proxyModel->selectionModel()->isSelected(ix)) { std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); parentFolder = std::static_pointer_cast(currentItem->getEnclosingFolder()); } return parentFolder->clipId(); } diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index cc0a18e95..8cd6246bc 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1227 +1,1227 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clip.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, producer) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); self->m_effectStack->importEffects(producer, true); model->loadSubClips(id, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone."))); return self; } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, model)); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::modelChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](){ if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); /*connect(m_effectStack.get(), &EffectStackModel::modelChanged, [&](){ qDebug()<<"/ / / STACK CHANGED"; updateChildProducers(); });*/ } QString ProjectClip::getToolTip() const { return url(); } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail(QVariantList audioLevels) { std::swap(audioFrameCache, audioLevels); // avoid second copy m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } updateTimelineClips({TimelineModel::AudioLevelsRole}); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } int ProjectClip::frameDuration() const { GenTime d = duration(); return d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // TODO: check if another load job is running? QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta) { getProducerXML(document, includeMeta); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(std::move(producer)); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) < 8 || getProducerIntProperty(QStringLiteral("meta.media.height")) < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video)) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer Clip clip(*prod.get()); m_thumbsProducer = std::make_shared(clip.softClone(ClipController::getPassPropertiesList())); Mlt::Filter scaler(*prod->profile(), "swscale"); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(converter); } else { m_thumbsProducer = cloneProducer(pCore->thumbProfile()); } return m_thumbsProducer; } void ProjectClip::createVideoMasterProducer() { if (!m_videoProducer) { m_videoProducer = std::shared_ptr(m_masterProducer->cut()); if (hasAudio()) { // disable audio but activate video m_videoProducer->set("set.test_audio", 1); m_videoProducer->set("set.test_image", 0); } } } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(&pCore->getCurrentProfile()->profile()); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int clipId, PlaylistState::ClipState state, double speed) { qDebug() << "producer request"< 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (m_audioProducers.count(clipId) == 0) { m_audioProducers[clipId] = cloneProducer(&pCore->getCurrentProfile()->profile()); m_audioProducers[clipId]->set("set.test_audio", 0); m_audioProducers[clipId]->set("set.test_image", 1); m_effectStack->addService(m_audioProducers[clipId]); } return std::shared_ptr(m_audioProducers[clipId]->cut()); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } if (state == PlaylistState::VideoOnly) { // we return the video producer createVideoMasterProducer(); int duration = m_masterProducer->get_int("kdenlive:duration"); return std::shared_ptr(m_videoProducer->cut(-1, duration > 0 ? duration: -1)); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->get_int("kdenlive:duration"); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration: -1)); } // in that case, we need to create a warp producer, if we don't have one if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QLocale locale; QString resource = QString("timewarp:%1:%2").arg(locale.toString(speed)).arg(originalProducer()->get("resource")); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), resource.toUtf8().constData())); qDebug() << "new producer!"; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding warpProducer->set("length", double(original_length) / speed); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; double timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state == PlaylistState::AudioOnly || !qFuzzyIsNull(timeWarp)) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(clipId, state, speed)->cut(in, out)); return {prod, false}; } // if it's a video or disabled clip, we must make sure that its master clip matches our video master if (state == PlaylistState::VideoOnly && !m_videoProducer) { qDebug() << "Warning: weird, we found a video clip whose master is already loaded but we don't have any yet"; createVideoMasterProducer(); return {std::shared_ptr(m_videoProducer->cut(in, out)), false}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } if (state == PlaylistState::VideoOnly && QString::fromUtf8(m_videoProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { qDebug() << "Warning: weird, we found a video clip whose master is already loaded but doesn't match ours"; return {std::shared_ptr(m_videoProducer->cut(in, out)), false}; } if (state == PlaylistState::Disabled && QString::fromUtf8(m_disabledProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but doesn't match ours"; return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (state == PlaylistState::AudioOnly) { m_audioProducers[clipId] = std::shared_ptr(&master->parent()); m_effectStack->addService(m_audioProducers[clipId]); return {master, true}; } if (!qFuzzyIsNull(timeWarp)) { m_timewarpProducers[clipId] = std::shared_ptr(&master->parent()); m_effectStack->addService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly && !m_videoProducer) { // good, we found a master video producer, and we didn't have any m_videoProducer.reset(master->parent().cut()); m_effectStack->addService(m_videoProducer); return {master, true}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { // good, we found a master disabled producer, and we didn't have any m_disabledProducer.reset(master->parent().cut()); m_effectStack->addService(m_disabledProducer); return {master, true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; return {std::shared_ptr(m_videoProducer->cut(in, out)), false}; } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; exit(1); double speed = 1.0; if (QString::fromUtf8(master->get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(clipId, state, speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } /* std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { std::shared_ptr prod(originalProducer()->cut()); int length = getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (length > 0) { prod->set_in_and_out(0, length); } return prod; } if (state == PlaylistState::VideoOnly) { if (m_timelineProducers.count(0) > 0) { return std::shared_ptr(m_timelineProducers.find(0)->second->cut()); } std::shared_ptr videoProd = cloneProducer(); videoProd->set("audio_index", -1); m_timelineProducers[0] = videoProd; return std::shared_ptr(videoProd->cut()); } if (state == PlaylistState::AudioOnly) { if (m_timelineProducers.count(-track) > 0) { return std::shared_ptr(m_timelineProducers.find(-track)->second->cut()); } std::shared_ptr audioProd = cloneProducer(); audioProd->set("video_index", -1); m_timelineProducers[-track] = audioProd; return std::shared_ptr(audioProd->cut()); } if (m_timelineProducers.count(track) > 0) { return std::shared_ptr(m_timelineProducers.find(track)->second->cut()); } std::shared_ptr normalProd = cloneProducer(); m_timelineProducers[track] = normalProd; return std::shared_ptr(normalProd->cut()); }*/ std::shared_ptr ProjectClip::cloneProducer(Mlt::Profile *destProfile) { Mlt::Consumer c(*m_masterProducer->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(destProfile ? *destProfile : *m_masterProducer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } return prod; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } /*void ProjectClip::setZone(const QPoint &zone) { m_zone = zone; }*/ QPoint ProjectClip::zone() const { int x = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int y = getProducerIntProperty(QStringLiteral("kdenlive:zone_out")); if (y <= x) { y = getFramePlaytime(); } return QPoint(x, y); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug()<<"// SETTING CLIP PROPERTIES: "< i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; // Some properties also need to be passed to track producers QStringList timelineProperties; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } timelineProperties << QStringLiteral("force_aspect_ratio") << QStringLiteral("video_index") << QStringLiteral("audio_index") << QStringLiteral("set.force_full_luma") << QStringLiteral("full_luma") << QStringLiteral("threads") << QStringLiteral("force_colorspace") << QStringLiteral("force_tff") << QStringLiteral("force_progressive") << QStringLiteral("force_fps"); QStringList keys; keys << QStringLiteral("luma_duration") << QStringLiteral("luma_file") << QStringLiteral("fade") << QStringLiteral("ttl") << QStringLiteral("softness") << QStringLiteral("crop") << QStringLiteral("animation"); QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); reloadProducer(); } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (properties.contains(QStringLiteral("resource")) || properties.contains(QStringLiteral("templatetext")) || properties.contains(QStringLiteral("autorotate"))) { // Clip resource changed, update thumbnail if (m_clipType != ClipType::Color) { reloadProducer(); } else { reload = true; updateRoles << TimelineModel::ResourceRole; } } if (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty()) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (reload) { // producer has changed, refresh monitor and thumbnail reloadProducer(refreshOnly); if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->updateTimelineProducers(m_binId, passProperties); } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); ClipPropertiesController *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); return panel; } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); qDebug() << "Setting parent to " << item->clipId(); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } void ProjectClip::slotExtractImage(const QList &frames) { QMutexLocker lock(&m_thumbMutex); for (int i = 0; i < frames.count(); i++) { if (!m_requestedThumbs.contains(frames.at(i))) { m_requestedThumbs << frames.at(i); } } qSort(m_requestedThumbs); if (!m_thumbThread.isRunning()) { m_thumbThread = QtConcurrent::run(this, &ProjectClip::doExtractImage); } } void ProjectClip::doExtractImage() { // TODO refac: we can probably move that into a ThumbJob std::shared_ptr prod = thumbProducer(); if (prod == nullptr || !prod->is_valid()) { return; } int frameWidth = 150 * prod->profile()->dar() + 0.5; bool ok = false; auto ptr = m_model.lock(); Q_ASSERT(ptr); QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); int max = prod->get_length(); while (!m_requestedThumbs.isEmpty()) { m_thumbMutex.lock(); int pos = m_requestedThumbs.takeFirst(); m_thumbMutex.unlock(); if (ok && thumbFolder.exists(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"))) { emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png")))); continue; } if (pos >= max) { pos = max - 1; } const QString path = url() + QLatin1Char('_') + QString::number(pos); QImage img; if (ThumbnailCache::get()->hasThumbnail(clipId(), pos, true)) { img = ThumbnailCache::get()->getThumbnail(clipId(), pos, true); } if (!img.isNull()) { emit thumbReady(pos, img); continue; } prod->seek(pos); Mlt::Frame *frame = prod->get_frame(); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (frame->is_valid()) { img = KThumb::getFrame(frame, frameWidth, 150, !qFuzzyCompare(prod->profile()->sar(),1)); ThumbnailCache::get()->storeThumbnail(clipId(), pos, img, false); emit thumbReady(pos, img); } delete frame; } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath() { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } bool ProjectClip::isTransparent() const { if (m_clipType == ClipType::Text) { return true; } return m_clipType == ClipType::Image && getProducerIntProperty(QStringLiteral("kdenlive:transparency")) == 1; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (std::map>::const_iterator it = m_registeredClips.begin(); it != m_registeredClips.end(); ++it) { ids.push_back(it->first); } return ids; } -bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) +bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo, Updates &list) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { - timeline->requestItemDeletion(clip.first, undo, redo); + timeline->requestItemDeletion(clip.first, undo, redo, list); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } - return AbstractProjectItem::selfSoftDelete(undo, redo); + return AbstractProjectItem::selfSoftDelete(undo, redo, list); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::updateChildProducers() { // TODO refac: the effect should be managed by an effectstack on the master /* // pass effect stack on all child producers QMutexLocker locker(&m_producerMutex); for (const auto &clip : m_timelineProducers) { if (auto producer = clip.second) { Clip clp(producer->parent()); clp.deleteEffects(); clp.replaceEffects(*m_masterProducer); } } */ } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(QVector roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } diff --git a/src/bin/projectclip.h b/src/bin/projectclip.h index 316be590f..a579b30ee 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -1,281 +1,281 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 PROJECTCLIP_H #define PROJECTCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include "mltcontroller/clipcontroller.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include class AudioStreamInfo; class ClipPropertiesController; class MarkerListModel; class ProjectFolder; class ProjectSubClip; class QDomElement; class QUndoCommand; namespace Mlt { class Producer; class Properties; } // namespace Mlt /** * @class ProjectClip * @brief Represents a clip in the project (not timeline). * It will be displayed as a bin item that can be dragged onto the timeline. * A single bin clip can be inserted several times on the timeline, and the ProjectClip * keeps track of all the ids of the corresponding ClipModel. * Note that because of a limitation in melt and AvFilter, it is currently difficult to * mix the audio of two producers that are cut from the same master producer * (that produces small but noticeable clicking artifacts) * To workaround this, we need to have a master clip for each instance of the audio clip in the timeline. This class is tracking them all. This track also holds * a master clip for each clip where the timewarp producer has been applied */ class ProjectClip : public AbstractProjectItem, public ClipController { Q_OBJECT public: friend class Bin; friend bool TimelineModel::checkConsistency(); // for testing /** * @brief Constructor; used when loading a project and the producer is already available. */ static std::shared_ptr construct(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer); /** * @brief Constructor. * @param description element describing the clip; the "kdenlive:id" attribute and "resource" property are used */ static std::shared_ptr construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); protected: ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer); ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); public: virtual ~ProjectClip(); void reloadProducer(bool refreshOnly = false); /** @brief Returns a unique hash identifier used to store clip thumbnails. */ // virtual void hash() = 0; /** @brief Returns this if @param id matches the clip's id or nullptr otherwise. */ std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr getSubClip(int in, int out); /** @brief Returns this if @param ix matches the clip's index or nullptr otherwise. */ std::shared_ptr clipAt(int ix) override; /** @brief Returns the clip type as defined in definitions.h */ ClipType::ProducerType clipType() const; - bool selfSoftDelete(Fun &undo, Fun &redo) override; + bool selfSoftDelete(Fun &undo, Fun &redo, Updates &list) override; /** @brief Check if clip has a parent folder with id id */ bool hasParent(const QString &id) const; ClipPropertiesController *buildProperties(QWidget *parent); QPoint zone() const override; /** @brief Returns true if we want to add an affine transition in timeline when dropping this clip. */ bool isTransparent() const; /** @brief Returns whether this clip has a url (=describes a file) or not. */ bool hasUrl() const; /** @brief Returns the clip's url. */ const QString url() const; /** @brief Returns the clip's duration. */ GenTime duration() const; int frameDuration() const; /** @brief Returns the original clip's fps. */ double getOriginalFps() const; bool rename(const QString &name, int column) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; QVariant getData(DataType type) const override; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); QPixmap thumbnail(int width, int height); /** @brief Sets the MLT producer associated with this clip * @param producer The producer * @param replaceProducer If true, we replace existing producer with this one * @returns true if producer was changed * . */ bool setProducer(std::shared_ptr producer, bool replaceProducer); /** @brief Returns true if this clip already has a producer. */ bool isReady() const; /** @brief Returns this clip's producer. */ std::shared_ptr thumbProducer(); /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** @brief Set properties on this clip. TODO: should we store all in MLT or use extra m_properties ?. */ void setProperties(const QMap &properties, bool refreshPanel = false); /** @brief Get an XML property from MLT produced xml. */ static QString getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue = QString()); QString getToolTip() const override; /** @brief The clip hash created from the clip's resource. */ const QString hash(); /** @brief Returns true if we are using a proxy for this clip. */ bool hasProxy() const; /** Cache for every audio Frame with 10 Bytes */ /** format is frame -> channel ->bytes */ QVariantList audioFrameCache; bool audioThumbCreated() const; void setWaitingStatus(const QString &id); /** @brief Returns true if the clip matched a condition, for example vcodec=mpeg1video. */ bool matches(const QString &condition); /** @brief Returns the number of audio channels. */ int audioChannels() const; /** @brief get data analysis value. */ QStringList updatedAnalysisData(const QString &name, const QString &data, int offset); QMap analysisData(bool withPrefix = false); /** @brief Returns the list of this clip's subclip's ids. */ QStringList subClipIds() const; /** @brief Delete cached audio thumb - needs to be recreated */ void discardAudioThumb(); /** @brief Get path for this clip's audio thumbnail */ const QString getAudioThumbPath(); /** @brief Returns true if this producer has audio and can be splitted on timeline*/ bool isSplittable() const; /** @brief Returns true if a clip corresponding to this bin is inserted in a timeline. Note that this function does not account for children, use TreeItem::accumulate if you want to get that information as well. */ bool isIncludedInTimeline() override; /** @brief Returns a list of all timeline clip ids for this bin clip */ QList timelineInstances() const; /** @brief This function returns a cut to the master producer associated to the timeline clip with given ID. Each clip must have a different master producer (see comment of the class) */ std::shared_ptr getTimelineProducer(int clipId, PlaylistState::ClipState st, double speed = 1.0); /* @brief This function should only be used at loading. It takes a producer that was read from mlt, and checks whether the master producer is already in use. If yes, then we must create a new one, because of the mixing bug. In any case, we return a cut of the master that can be used in the timeline The bool returned has the following sementic: - if true, then the returned cut still possibly has effect on it. You need to rebuild the effectStack based on this - if false, the the returned cut don't have effects anymore (it's a fresh one), so you need to reload effects from the old producer */ std::pair, bool> giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state); std::shared_ptr cloneProducer(Mlt::Profile *destProfile = nullptr); void updateTimelineClips(QVector roles); protected: friend class ClipModel; /** @brief This is a call-back called by a ClipModel when it is created @param timeline ptr to the pointer in which this ClipModel is inserted @param clipId id of the inserted clip */ void registerTimelineClip(std::weak_ptr timeline, int clipId); /* @brief update the producer to reflect new parent folder */ void updateParent(std::shared_ptr parent) override; /** @brief This is a call-back called by a ClipModel when it is deleted @param clipId id of the deleted clip */ void deregisterTimelineClip(int clipId); void emitProducerChanged(const QString &id, const std::shared_ptr &producer) override { emit producerChanged(id, producer); }; /** @brief Replace instance of this clip in timeline */ void updateChildProducers(); void replaceInTimeline(); void connectEffectStack(); public slots: /* @brief Store the audio thumbnails once computed. Note that the parameter is a value and not a reference, fill free to use it as a sink (use std::move to * avoid copy). */ void updateAudioThumbnail(QVariantList audioLevels); /** @brief Extract image thumbnails for timeline. */ void slotExtractImage(const QList &frames); private: /** @brief Generate and store file hash if not available. */ const QString getFileHash(); /** @brief Store clip url temporarily while the clip controller has not been created. */ QString m_temporaryUrl; std::shared_ptr m_thumbsProducer; QMutex m_producerMutex; QMutex m_thumbMutex; QFuture m_thumbThread; QList m_requestedThumbs; const QString geometryWithOffset(const QString &data, int offset); void doExtractImage(); // This is a helper function that creates the video producer. This is a clone of the original one, with audio disabled void createVideoMasterProducer(); // This is a helper function that creates the disabled producer. This is a clone of the original one, with audio and video disabled void createDisabledMasterProducer(); std::map> m_registeredClips; // the following holds a producer for each audio clip in the timeline // keys are the id of the clips in the timeline, values are their values std::unordered_map> m_audioProducers; std::unordered_map> m_timewarpProducers; std::shared_ptr m_videoProducer, m_disabledProducer; signals: void producerChanged(const QString &, const std::shared_ptr &); void refreshPropertiesPanel(); void refreshAnalysisPanel(); void refreshClipDisplay(); void thumbReady(int, const QImage &); /** @brief Clip is ready, load properties. */ void loadPropertiesPanel(); }; #endif diff --git a/src/bin/projectitemmodel.cpp b/src/bin/projectitemmodel.cpp index b78627561..56300a56e 100644 --- a/src/bin/projectitemmodel.cpp +++ b/src/bin/projectitemmodel.cpp @@ -1,885 +1,890 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 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 "projectitemmodel.h" #include "abstractprojectitem.h" #include "binplaylist.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "filewatcher.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectsubclip.h" #include "xml/xml.hpp" #include #include #include #include #include #include ProjectItemModel::ProjectItemModel(QObject *parent) : AbstractTreeModel(parent) , m_lock(QReadWriteLock::Recursive) , m_binPlaylist(new BinPlaylist()) , m_fileWatcher(new FileWatcher()) , m_nextId(1) , m_blankThumb() { QPixmap pix(QSize(160, 90)); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); connect(m_fileWatcher.get(), &FileWatcher::binClipModified, this, &ProjectItemModel::reloadClip); connect(m_fileWatcher.get(), &FileWatcher::binClipWaiting, this, &ProjectItemModel::setClipWaiting); } std::shared_ptr ProjectItemModel::construct(QObject *parent) { std::shared_ptr self(new ProjectItemModel(parent)); self->rootItem = ProjectFolder::construct(self); return self; } ProjectItemModel::~ProjectItemModel() {} int ProjectItemModel::mapToColumn(int column) const { switch (column) { case 0: return AbstractProjectItem::DataName; break; case 1: return AbstractProjectItem::DataDate; break; case 2: return AbstractProjectItem::DataDescription; break; default: return AbstractProjectItem::DataName; } } QVariant ProjectItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!index.isValid()) { return QVariant(); } if (role == Qt::DisplayRole || role == Qt::EditRole) { std::shared_ptr item = getBinItemByIndex(index); auto type = static_cast(mapToColumn(index.column())); QVariant ret = item->getData(type); return ret; } if (role == Qt::DecorationRole) { if (index.column() != 0) { return QVariant(); } // Data has to be returned as icon to allow the view to scale it std::shared_ptr item = getBinItemByIndex(index); QVariant thumb = item->getData(AbstractProjectItem::DataThumbnail); QIcon icon; if (thumb.canConvert()) { icon = thumb.value(); } else { qDebug() << "ERROR: invalid icon found"; } return icon; } std::shared_ptr item = getBinItemByIndex(index); return item->getData((AbstractProjectItem::DataType)role); } bool ProjectItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { QWriteLocker locker(&m_lock); std::shared_ptr item = getBinItemByIndex(index); if (item->rename(value.toString(), index.column())) { emit dataChanged(index, index, QVector() << role); return true; } // Item name was not changed return false; } Qt::ItemFlags ProjectItemModel::flags(const QModelIndex &index) const { /*return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;*/ if (!index.isValid()) { return Qt::ItemIsDropEnabled; } std::shared_ptr item = getBinItemByIndex(index); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); switch (type) { case AbstractProjectItem::FolderItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::ClipItem: if (!item->statusReady()) { return Qt::ItemIsSelectable; } return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::SubClipItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; break; case AbstractProjectItem::FolderUpItem: return Qt::ItemIsEnabled; break; default: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } } // cppcheck-suppress unusedFunction bool ProjectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row) Q_UNUSED(column) if (action == Qt::IgnoreAction) { return true; } if (data->hasUrls()) { emit itemDropped(data->urls(), parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/producerslist"))) { // Dropping an Bin item const QStringList ids = QString(data->data(QStringLiteral("kdenlive/producerslist"))).split(QLatin1Char(';')); if (ids.constFirst().contains(QLatin1Char('/'))) { // subclip zone QStringList clipData = ids.constFirst().split(QLatin1Char('/')); if (clipData.length() >= 3) { QString id; return requestAddBinSubClip(id, clipData.at(1).toInt(), clipData.at(2).toInt(), QString(), clipData.at(0)); } else { // error, malformed clip zone, abort return false; } } else { emit itemDropped(ids, parent); } return true; } if (data->hasFormat(QStringLiteral("kdenlive/effect"))) { // Dropping effect on a Bin item QStringList effectData; effectData << QString::fromUtf8(data->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(data->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit effectDropped(effectData, parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/clip"))) { const QStringList list = QString(data->data(QStringLiteral("kdenlive/clip"))).split(QLatin1Char(';')); QString id; return requestAddBinSubClip(id, list.at(1).toInt(), list.at(2).toInt(), QString(), list.at(0)); } return false; } QVariant ProjectItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { QVariant columnName; switch (section) { case 0: columnName = i18n("Name"); break; case 1: columnName = i18n("Date"); break; case 2: columnName = i18n("Description"); break; default: columnName = i18n("Unknown"); break; } return columnName; } return QAbstractItemModel::headerData(section, orientation, role); } int ProjectItemModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return getBinItemByIndex(parent)->supportedDataCount(); } return std::static_pointer_cast(rootItem)->supportedDataCount(); } // cppcheck-suppress unusedFunction Qt::DropActions ProjectItemModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ProjectItemModel::mimeTypes() const { QStringList types; types << QStringLiteral("kdenlive/producerslist") << QStringLiteral("text/uri-list") << QStringLiteral("kdenlive/clip") << QStringLiteral("kdenlive/effect"); return types; } QMimeData *ProjectItemModel::mimeData(const QModelIndexList &indices) const { // Mime data is a list of id's separated by ';'. // Clip ids are represented like: 2 (where 2 is the clip's id) // Clip zone ids are represented like: 2/10/200 (where 2 is the clip's id, 10 and 200 are in and out points) // Folder ids are represented like: #2 (where 2 is the folder's id) auto *mimeData = new QMimeData(); QStringList list; int duration = 0; for (int i = 0; i < indices.count(); i++) { QModelIndex ix = indices.at(i); if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = getBinItemByIndex(ix); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); if (type == AbstractProjectItem::ClipItem) { list << item->clipId(); duration += (std::static_pointer_cast(item))->frameDuration(); } else if (type == AbstractProjectItem::SubClipItem) { QPoint p = item->zone(); list << std::static_pointer_cast(item)->getMasterClip()->clipId() + QLatin1Char('/') + QString::number(p.x()) + QLatin1Char('/') + QString::number(p.y()); } else if (type == AbstractProjectItem::FolderItem) { list << "#" + item->clipId(); } } if (!list.isEmpty()) { QByteArray data; data.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/producerslist"), data); qDebug() << "/// CLI DURATION: " << duration; mimeData->setText(QString::number(duration)); } return mimeData; } void ProjectItemModel::onItemUpdated(std::shared_ptr item, int role) { auto tItem = std::static_pointer_cast(item); auto ptr = tItem->parentItem().lock(); if (ptr) { auto index = getIndexFromItem(tItem); emit dataChanged(index, index, QVector() << role); } } void ProjectItemModel::onItemUpdated(const QString &binId, int role) { std::shared_ptr item = getItemByBinId(binId); if (item) { onItemUpdated(item, role); } } std::shared_ptr ProjectItemModel::getClipByBinID(const QString &binId) { if (binId.contains(QLatin1Char('_'))) { return getClipByBinID(binId.section(QLatin1Char('_'), 0, 0)); } for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } bool ProjectItemModel::hasClip(const QString &binId) { return getClipByBinID(binId) != nullptr; } std::shared_ptr ProjectItemModel::getFolderByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::FolderItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } std::shared_ptr ProjectItemModel::getItemByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == binId) { return c; } } return nullptr; } void ProjectItemModel::setBinEffectsEnabled(bool enabled) { return std::static_pointer_cast(rootItem)->setBinEffectsEnabled(enabled); } QStringList ProjectItemModel::getEnclosingFolderInfo(const QModelIndex &index) const { QStringList noInfo; noInfo << QString::number(-1); noInfo << QString(); if (!index.isValid()) { return noInfo; } std::shared_ptr currentItem = getBinItemByIndex(index); auto folder = currentItem->getEnclosingFolder(true); if ((folder == nullptr) || folder == rootItem) { return noInfo; } QStringList folderInfo; folderInfo << currentItem->clipId(); folderInfo << currentItem->name(); return folderInfo; } void ProjectItemModel::clean() { std::vector> toDelete; for (int i = 0; i < rootItem->childCount(); ++i) { toDelete.push_back(std::static_pointer_cast(rootItem->child(i))); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; for (const auto &child : toDelete) { - requestBinClipDeletion(child, undo, redo); + requestBinClipDeletion(child, undo, redo, list); } + ModelUpdater::applyUpdates(undo, redo, list); + Q_ASSERT(rootItem->childCount() == 0); m_nextId = 1; m_fileWatcher->clear(); } std::shared_ptr ProjectItemModel::getRootFolder() const { return std::static_pointer_cast(rootItem); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; loadSubClips(id, dataMap, undo, redo); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo) { std::shared_ptr clip = getClipByBinID(id); if (!clip) { return; } QMapIterator i(dataMap); QList missingThumbs; int maxFrame = clip->duration().frames(pCore->getCurrentFps()) - 1; while (i.hasNext()) { i.next(); if (!i.value().contains(QLatin1Char(';'))) { // Problem, the zone has no in/out points continue; } int in = i.value().section(QLatin1Char(';'), 0, 0).toInt(); int out = i.value().section(QLatin1Char(';'), 1, 1).toInt(); if (maxFrame > 0) { out = qMin(out, maxFrame); } QString subId; requestAddBinSubClip(subId, in, out, i.key(), id, undo, redo); } } std::shared_ptr ProjectItemModel::getBinItemByIndex(const QModelIndex &index) const { return std::static_pointer_cast(getItemById((int)index.internalId())); } -bool ProjectItemModel::requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo) +bool ProjectItemModel::requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(clip); if (!clip) return false; int parentId = -1; if (auto ptr = clip->parent()) parentId = ptr->getId(); - clip->selfSoftDelete(undo, redo); + clip->selfSoftDelete(undo, redo, list); int id = clip->getId(); Fun operation = removeItem_lambda(id); Fun reverse = addItem_lambda(clip, parentId); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } void ProjectItemModel::registerItem(const std::shared_ptr &item) { auto clip = std::static_pointer_cast(item); m_binPlaylist->manageBinItemInsertion(clip); AbstractTreeModel::registerItem(item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = std::static_pointer_cast(clip); updateWatcher(clipItem); } } void ProjectItemModel::deregisterItem(int id, TreeItem *item) { auto clip = static_cast(item); m_binPlaylist->manageBinItemDeletion(clip); // TODO : here, we should suspend jobs belonging to the item we delete. They can be restarted if the item is reinserted by undo AbstractTreeModel::deregisterItem(id, item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = static_cast(clip); m_fileWatcher->addFile(clipItem->clipId(), clipItem->clipUrl()); } } int ProjectItemModel::getFreeFolderId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } int ProjectItemModel::getFreeClipId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } bool ProjectItemModel::addItem(std::shared_ptr item, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); std::shared_ptr parentItem = getItemByBinId(parentId); if (!parentItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR IN PARENT FOLDER"; return false; } if (item->itemType() == AbstractProjectItem::ClipItem && parentItem->itemType() != AbstractProjectItem::FolderItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting clip: a clip should be inserted in a folder"; return false; } if (item->itemType() == AbstractProjectItem::SubClipItem && parentItem->itemType() != AbstractProjectItem::ClipItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting subclip: a subclip should be inserted in a clip"; return false; } Fun operation = addItem_lambda(item, parentItem->getId()); int itemId = item->getId(); Fun reverse = removeItem_lambda(itemId); bool res = operation(); Q_ASSERT(item->isInModel()); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } bool ProjectItemModel::requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (!id.isEmpty() && !isIdFree(id)) { id = QString(); } if (id.isEmpty()) { id = QString::number(getFreeFolderId()); } std::shared_ptr new_folder = ProjectFolder::construct(id, name, std::static_pointer_cast(shared_from_this())); return addItem(new_folder, parentId, undo, redo); } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo) { qDebug() << "/////////// requestAddBinClip" << parentId; QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = Xml::getTagContentByAttribute(description, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id"), QStringLiteral("-1")); if (id == QStringLiteral("-1") || !isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); qDebug() << "/////////// found id" << id; std::shared_ptr new_clip = ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast(shared_from_this())); qDebug() << "/////////// constructed "; bool res = addItem(new_clip, parentId, undo, redo); qDebug() << "/////////// added " << res; if (res) { int loadJob = pCore->jobManager()->startJob({id}, -1, QString(), description); pCore->jobManager()->startJob({id}, loadJob, QString(), 150, 0, true); pCore->jobManager()->startJob({id}, loadJob, QString()); } return res; } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinClip(id, description, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, undoText.isEmpty() ? i18n("Add bin clip") : undoText); } return res; } bool ProjectItemModel::requestAddBinClip(QString &id, std::shared_ptr producer, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(producer->get_int("kdenlive:id")); if (!isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); std::shared_ptr new_clip = ProjectClip::construct(id, m_blankThumb, std::static_pointer_cast(shared_from_this()), producer); bool res = addItem(new_clip, parentId, undo, redo); if (res) { int blocking = pCore->jobManager()->getBlockingJobId(id, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, blocking, QString(), 150, -1, true); pCore->jobManager()->startJob({id}, blocking, QString()); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(getFreeClipId()); } Q_ASSERT(!id.isEmpty() && isIdFree(id)); auto clip = getClipByBinID(parentId); Q_ASSERT(clip->itemType() == AbstractProjectItem::ClipItem); auto tc = pCore->currentDoc()->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode()); std::shared_ptr new_clip = ProjectSubClip::construct(id, clip, std::static_pointer_cast(shared_from_this()), in, out, tc, zoneName); bool res = addItem(new_clip, parentId, undo, redo); if (res) { int parentJob = pCore->jobManager()->getBlockingJobId(parentId, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, parentJob, QString(), 150, -1, true); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinSubClip(id, in, out, zoneName, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Add a sub clip")); } return res; } Fun ProjectItemModel::requestRenameFolder_lambda(std::shared_ptr folder, const QString &newName) { int id = folder->getId(); return [this, id, newName]() { auto currentFolder = std::static_pointer_cast(m_allItems[id].lock()); if (!currentFolder) { return false; } currentFolder->setName(newName); auto index = getIndexFromItem(currentFolder); emit dataChanged(index, index, {AbstractProjectItem::DataName}); return true; }; } bool ProjectItemModel::requestRenameFolder(std::shared_ptr folder, const QString &name, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); QString oldName = folder->name(); auto operation = requestRenameFolder_lambda(folder, name); if (operation()) { auto reverse = requestRenameFolder_lambda(folder, oldName); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } bool ProjectItemModel::requestRenameFolder(std::shared_ptr folder, const QString &name) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestRenameFolder(folder, name, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Rename Folder")); } return res; } bool ProjectItemModel::requestCleanup() { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; bool res = true; std::vector> to_delete; // Iterate to find clips that are not in timeline for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && !c->isIncludedInTimeline()) { to_delete.push_back(c); } } // it is important to execute deletion in a separate loop, because otherwise // the iterators of m_allItems get messed up for (const auto &c : to_delete) { - res = requestBinClipDeletion(c, undo, redo); + res = requestBinClipDeletion(c, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Clean Project")); return true; } std::vector ProjectItemModel::getAllClipIds() const { std::vector result; for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem) { result.push_back(c->clipId()); } } return result; } bool ProjectItemModel::loadFolders(Mlt::Properties &folders) { // At this point, we expect the folders properties to have a name of the form "x.y" where x is the id of the parent folder and y the id of the child. // Note that for root folder, x = -1 // The value of the property is the name of the child folder std::unordered_map> downLinks; // key are parents, value are children std::unordered_map upLinks; // key are children, value are parent std::unordered_map newIds; // we store the correspondance to the new ids std::unordered_map folderNames; newIds[-1] = getRootFolder()->clipId(); if (folders.count() == 0) return true; for (int i = 0; i < folders.count(); i++) { QString folderName = folders.get(i); QString id = folders.get_name(i); int parentId = id.section(QLatin1Char('.'), 0, 0).toInt(); int folderId = id.section(QLatin1Char('.'), 1, 1).toInt(); downLinks[parentId].push_back(folderId); upLinks[folderId] = parentId; folderNames[folderId] = folderName; qDebug() << "Found folder " << folderId << "name = " << folderName << "parent=" << parentId; } // In case there are some non-existant parent, we fall back to root for (const auto &f : downLinks) { if (upLinks.count(f.first) == 0) { upLinks[f.first] = -1; } if (f.first != -1 && downLinks.count(upLinks[f.first]) == 0) { qDebug() << "Warning: parent folder " << upLinks[f.first] << "for folder" << f.first << "is invalid. Folder will be placed in topmost directory."; upLinks[f.first] = -1; } } // We now do a BFS to construct the folders in order Q_ASSERT(downLinks.count(-1) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::queue queue; std::unordered_set seen; queue.push(-1); while (!queue.empty()) { int current = queue.front(); seen.insert(current); queue.pop(); if (current != -1) { QString id = QString::number(current); bool res = requestAddFolder(id, folderNames[current], newIds[upLinks[current]], undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } newIds[current] = id; } for (int c : downLinks[current]) { queue.push(c); } } return true; } bool ProjectItemModel::isIdFree(const QString &id) const { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == id) { return false; } } return true; } void ProjectItemModel::loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp) { clean(); Mlt::Properties retainList((mlt_properties)documentTractor->get_data("xml_retain")); qDebug() << "Loading bin playlist..."; if (retainList.is_valid()) { qDebug() << "retain is valid"; Mlt::Playlist playlist((mlt_playlist) retainList.get_data(BinPlaylist::binPlaylistId.toUtf8().constData())); if (playlist.is_valid() && playlist.type() == playlist_type) { qDebug() << "playlist is valid"; // Load bin clips qDebug() << "init bin"; // Load folders Mlt::Properties folderProperties; Mlt::Properties playlistProps(playlist.get_properties()); folderProperties.pass_values(playlistProps, "kdenlive:folder."); loadFolders(folderProperties); // Read notes QString notes = playlistProps.get("kdenlive:documentnotes"); pCore->projectManager()->setDocumentNotes(notes); Fun undo = []() { return true; }; Fun redo = []() { return true; }; qDebug() << "Found " << playlist.count() << "clips"; int max = playlist.count(); for (int i = 0; i < max; i++) { QScopedPointer prod(playlist.get_clip(i)); std::shared_ptr producer(new Mlt::Producer(prod->parent())); qDebug() << "dealing with bin clip" << i; if (producer->is_blank() || !producer->is_valid()) { qDebug() << "producer is not valid or blank"; continue; } QString id = qstrdup(producer->get("kdenlive:id")); QString parentId = qstrdup(producer->get("kdenlive:folderid")); if (parentId.isEmpty()) { parentId = QStringLiteral("-1"); } qDebug() << "clip id" << id; if (id.contains(QLatin1Char('_'))) { // TODO refac ? /* // This is a track producer QString mainId = id.section(QLatin1Char('_'), 0, 0); // QString track = id.section(QStringLiteral("_"), 1, 1); if (m_clipList.contains(mainId)) { // The controller for this track producer already exists } else { // Create empty controller for this clip requestClipInfo info; info.imageHeight = 0; info.clipId = id; info.replaceProducer = true; emit slotProducerReady(info, ClipController::mediaUnavailable); } */ } else { QString newId = isIdFree(id) ? id : QString::number(getFreeClipId()); producer->set("_kdenlive_processed", 1); requestAddBinClip(newId, producer, parentId, undo, redo); binIdCorresp[id] = newId; qDebug() << "Loaded clip " << id << "under id" << newId; } } } } m_binPlaylist->setRetainIn(modelTractor); } /** @brief Save document properties in MLT's bin playlist */ void ProjectItemModel::saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel) { m_binPlaylist->saveDocumentProperties(props, metadata, guideModel); } void ProjectItemModel::saveProperty(const QString &name, const QString &value) { m_binPlaylist->saveProperty(name, value); } QMap ProjectItemModel::getProxies(const QString &root) { return m_binPlaylist->getProxies(root); } void ProjectItemModel::reloadClip(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->reloadProducer(); } } void ProjectItemModel::setClipWaiting(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void ProjectItemModel::updateWatcher(std::shared_ptr clipItem) { if (clipItem->clipType() == ClipType::AV || clipItem->clipType() == ClipType::Audio || clipItem->clipType() == ClipType::Image || clipItem->clipType() == ClipType::Video || clipItem->clipType() == ClipType::Playlist || clipItem->clipType() == ClipType::TextTemplate) { m_fileWatcher->removeFile(clipItem->clipId()); m_fileWatcher->addFile(clipItem->clipId(), clipItem->clipUrl()); } } diff --git a/src/bin/projectitemmodel.h b/src/bin/projectitemmodel.h index af7e58c89..bb8862d04 100644 --- a/src/bin/projectitemmodel.h +++ b/src/bin/projectitemmodel.h @@ -1,249 +1,250 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 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 PROJECTITEMMODEL_H #define PROJECTITEMMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "definitions.h" +#include "timeline2/model/modelupdater.hpp" #include "undohelper.hpp" #include #include #include #include class AbstractProjectItem; class BinPlaylist; class FileWatcher; class MarkerListModel; class ProjectClip; class ProjectFolder; namespace Mlt { class Producer; class Properties; class Tractor; } // namespace Mlt /** * @class ProjectItemModel * @brief Acts as an adaptor to be able to use BinModel with item views. */ class ProjectItemModel : public AbstractTreeModel { Q_OBJECT protected: explicit ProjectItemModel(QObject *parent); public: static std::shared_ptr construct(QObject *parent = nullptr); ~ProjectItemModel(); friend class ProjectClip; /** @brief Returns a clip from the hierarchy, given its id */ std::shared_ptr getClipByBinID(const QString &binId); /** @brief Helper to check whether a clip with a given id exists */ bool hasClip(const QString &binId); /** @brief Gets a folder by its id. If none is found, the root is returned */ std::shared_ptr getFolderByBinId(const QString &binId); /** @brief Gets any item by its id. */ std::shared_ptr getItemByBinId(const QString &binId); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); /** @brief Returns some info about the folder containing the given index */ QStringList getEnclosingFolderInfo(const QModelIndex &index) const; /** @brief Deletes all element and start a fresh model */ void clean(); /** @brief Returns the id of all the clips (excluding folders) */ std::vector getAllClipIds() const; /** @brief Convenience method to access root folder */ std::shared_ptr getRootFolder() const; /** @brief Create the subclips defined in the parent clip. @param id is the id of the parent clip @param data is a definition of the subclips (keys are subclips' names, value are "in:out")*/ void loadSubClips(const QString &id, const QMap &data); void loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo); /* @brief Convenience method to retrieve a pointer to an element given its index */ std::shared_ptr getBinItemByIndex(const QModelIndex &index) const; /* @brief Load the folders given the property containing them */ bool loadFolders(Mlt::Properties &folders); /* @brief Parse a bin playlist from the document tractor and reconstruct the tree */ void loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp); /** @brief Save document properties in MLT's bin playlist */ void saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel); /** @brief Save a property to main bin */ void saveProperty(const QString &name, const QString &value); /** @brief Returns item data depending on role requested */ QVariant data(const QModelIndex &index, int role) const override; /** @brief Called when user edits an item */ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; /** @brief Allow selection and drag & drop */ Qt::ItemFlags flags(const QModelIndex &index) const override; /** @brief Returns column names in case we want to use columns in QTreeView */ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; /** @brief Mandatory reimplementation from QAbstractItemModel */ int columnCount(const QModelIndex &parent = QModelIndex()) const override; /** @brief Returns the MIME type used for Drag actions */ QStringList mimeTypes() const override; /** @brief Create data that will be used for Drag events */ QMimeData *mimeData(const QModelIndexList &indices) const override; /** @brief Set size for thumbnails */ void setIconSize(QSize s); bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; Qt::DropActions supportedDropActions() const override; /* @brief Request deletion of a bin clip from the project bin @param clip : pointer to the clip to delete @param undo,redo: lambdas that are updated to accumulate operation. */ - bool requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo); + bool requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo, Updates &list); /* @brief Request creation of a bin folder @param id Id of the requested bin. If this is empty or invalid (already used, for example), it will be used as a return parameter to give the automatic bin id used. @param name Name of the folder @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo); /* @brief Request creation of a bin clip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param description Xml description of the clip @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText = QString()); /* @brief This is the addition function when we already have a producer for the clip*/ bool requestAddBinClip(QString &id, std::shared_ptr producer, const QString &parentId, Fun &undo, Fun &redo); /* @brief Create a subClip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param parentId Bin id of the parent clip @param in,out : zone that corresponds to the subclip @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId); /* @brief Request that a folder's name is changed @param clip : pointer to the folder to rename @param name: new name of the folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestRenameFolder(std::shared_ptr folder, const QString &name, Fun &undo, Fun &redo); /* Same functions but pushes the undo object directly */ bool requestRenameFolder(std::shared_ptr folder, const QString &name); /* @brief Request that the unused clips are deleted */ bool requestCleanup(); /* @brief Retrieves the next id available for attribution to a folder */ int getFreeFolderId(); /* @brief Retrieves the next id available for attribution to a clip */ int getFreeClipId(); /** @brief Retrieve a list of proxy/original urls */ QMap getProxies(const QString &root); /** @brief Request that the producer of a given clip is reloaded */ void reloadClip(const QString &binId); /** @brief Set the status of the clip to "waiting". This happens when the corresponding file has changed*/ void setClipWaiting(const QString &binId); protected: /* @brief Register the existence of a new element */ void registerItem(const std::shared_ptr &item) override; /* @brief Deregister the existence of a new element*/ void deregisterItem(int id, TreeItem *item) override; /* @brief Helper function to generate a lambda that rename a folder */ Fun requestRenameFolder_lambda(std::shared_ptr folder, const QString &newName); /* @brief Helper function to add a given item to the tree */ bool addItem(std::shared_ptr item, const QString &parentId, Fun &undo, Fun &redo); /* @brief Function to be called when the url of a clip changes */ void updateWatcher(std::shared_ptr item); public slots: /** @brief An item in the list was modified, notify */ void onItemUpdated(std::shared_ptr item, int role); void onItemUpdated(const QString &binId, int role); /** @brief Check whether a given id is currently used or not*/ bool isIdFree(const QString &id) const; private: /** @brief Return reference to column specific data */ int mapToColumn(int column) const; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::unique_ptr m_binPlaylist; std::unique_ptr m_fileWatcher; int m_nextId; QIcon m_blankThumb; signals: // thumbs of the given clip were modified, request update of the monitor if need be void refreshAudioThumbs(const QString &id); void refreshClip(const QString &id); void emitMessage(const QString &, int, MessageType); void updateTimelineProducers(const QString &id, const QMap &passProperties); void refreshPanel(const QString &id); void requestAudioThumbs(const QString &id, long duration); // TODO void markersNeedUpdate(const QString &id, const QList &); void itemDropped(const QStringList &, const QModelIndex &); void itemDropped(const QList &, const QModelIndex &); void effectDropped(const QStringList &, const QModelIndex &); void addClipCut(const QString &, int, int); }; #endif diff --git a/src/jobs/abstractclipjob.h b/src/jobs/abstractclipjob.h index cef50d682..213142a55 100644 --- a/src/jobs/abstractclipjob.h +++ b/src/jobs/abstractclipjob.h @@ -1,99 +1,100 @@ /*************************************************************************** * * * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef ABSTRACTCLIPJOB #define ABSTRACTCLIPJOB #include #include #include "definitions.h" +#include "timeline2/model/modelupdater.hpp" #include "undohelper.hpp" #include /** * @class AbstractClipJob * @brief This is the base class for all Kdenlive clip jobs. * */ struct Job_t; class AbstractClipJob : public QObject { Q_OBJECT public: enum JOBTYPE { NOJOBTYPE = 0, PROXYJOB = 1, CUTJOB = 2, STABILIZEJOB = 3, TRANSCODEJOB = 4, FILTERCLIPJOB = 5, THUMBJOB = 6, ANALYSECLIPJOB = 7, LOADJOB = 8, AUDIOTHUMBJOB = 9, SPEEDJOB = 10 }; AbstractClipJob(JOBTYPE type, const QString &id, QObject *parent = nullptr); virtual ~AbstractClipJob(); template static std::shared_ptr make(const QString &binId, Args &&... args) { auto m = std::make_shared(binId, std::forward(args)...); return m; } /** @brief Returns the id of the bin clip that this job is working on. */ const QString clipId() const; const QString getErrorMessage() const; const QString getLogDetails() const; virtual const QString getDescription() const = 0; virtual bool startJob() = 0; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult This methods return true on success */ - virtual bool commitResult(Fun &undo, Fun &redo) = 0; + virtual bool commitResult(Fun &undo, Fun &redo, Updates &list) = 0; // brief run a given job static bool execute(std::shared_ptr job); /* @brief return the type of this job */ JOBTYPE jobType() const; protected: QString m_clipId; QString m_errorMessage; QString m_logDetails; int m_addClipToProject; JOBTYPE m_jobType; bool m_resultConsumed{false}; signals: // send an int between 0 and 100 to reflect computation progress void jobProgress(int); }; #endif diff --git a/src/jobs/audiothumbjob.cpp b/src/jobs/audiothumbjob.cpp index 97fc2e0aa..1ec28e049 100644 --- a/src/jobs/audiothumbjob.cpp +++ b/src/jobs/audiothumbjob.cpp @@ -1,340 +1,340 @@ /*************************************************************************** * 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 "audiothumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "lib/audio/audioStreamInfo.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include #include #include #include AudioThumbJob::AudioThumbJob(const QString &binId) : AbstractClipJob(AUDIOTHUMBJOB, binId) , m_ffmpegProcess(nullptr) { } const QString AudioThumbJob::getDescription() const { return i18n("Extracting audio thumb from clip %1", m_clipId); } bool AudioThumbJob::computeWithMlt() { m_audioLevels.clear(); // MLT audio thumbs: slower but safer QString service = m_prod->get("mlt_service"); if (service == QLatin1String("avformat-novalidate")) { service = QStringLiteral("avformat"); } else if (service.startsWith(QLatin1String("xml"))) { service = QStringLiteral("xml-nogl"); } QScopedPointer audioProducer(new Mlt::Producer(*m_prod->profile(), service.toUtf8().constData(), m_prod->get("resource"))); if (!audioProducer->is_valid()) { return false; } audioProducer->set("video_index", "-1"); Mlt::Filter chans(*m_prod->profile(), "audiochannels"); Mlt::Filter converter(*m_prod->profile(), "audioconvert"); Mlt::Filter levels(*m_prod->profile(), "audiolevel"); audioProducer->attach(chans); audioProducer->attach(converter); audioProducer->attach(levels); int last_val = 0; double framesPerSecond = audioProducer->get_fps(); mlt_audio_format audioFormat = mlt_audio_s16; QStringList keys; keys.reserve(m_channels); for (int i = 0; i < m_channels; i++) { keys << "meta.media.audio_level." + QString::number(i); } for (int z = 0; z < m_lengthInFrames; ++z) { int val = (int)(100.0 * z / m_lengthInFrames); if (last_val != val) { emit jobProgress(val); last_val = val; } QScopedPointer mltFrame(audioProducer->get_frame()); if ((mltFrame != nullptr) && mltFrame->is_valid() && (mltFrame->get_int("test_audio") == 0)) { int samples = mlt_sample_calculator(float(framesPerSecond), m_frequency, z); mltFrame->get_audio(audioFormat, m_frequency, m_channels, samples); for (int channel = 0; channel < m_channels; ++channel) { double level = 256 * qMin(mltFrame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0); m_audioLevels << level; } } else if (!m_audioLevels.isEmpty()) { for (int channel = 0; channel < m_channels; channel++) { m_audioLevels << m_audioLevels.last(); } } } m_done = true; return true; } bool AudioThumbJob::computeWithFFMPEG() { m_audioLevels.clear(); QStringList args; std::vector> channelFiles; for (int i = 0; i < m_channels; i++) { std::unique_ptr channelTmpfile(new QTemporaryFile()); if (!channelTmpfile->open()) { m_errorMessage.append(i18n("Cannot create temporary file, check disk space and permissions\n")); return false; } channelTmpfile->close(); channelFiles.emplace_back(std::move(channelTmpfile)); } args << QStringLiteral("-i") << QUrl::fromLocalFile(m_prod->get("resource")).toLocalFile(); // Output progress info args << QStringLiteral("-progress"); #ifdef Q_OS_WIN args << QStringLiteral("-"); #else args << QStringLiteral("/dev/stdout"); #endif bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg")); args << QStringLiteral("-filter_complex:a"); if (m_channels == 1) { args << QStringLiteral("aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates"); args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[0]->fileName(); } else { QString aformat = QStringLiteral("[0:a%1]%2=100,channelsplit=channel_layout=%3") .arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) .arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates=") .arg(m_channels > 2 ? "5.1" : "stereo"); for (int i = 0; i < m_channels; ++i) { aformat.append(QStringLiteral("[0:%1]").arg(i)); } args << aformat; for (int i = 0; i < m_channels; i++) { // Channel 1 args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[size_t(i)]->fileName(); } } m_ffmpegProcess = new QProcess; m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args); connect(m_ffmpegProcess, &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); m_ffmpegProcess->waitForFinished(-1); if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) { int dataSize = 0; std::vector rawChannels; std::vector sourceChannels; for (size_t i = 0; i < channelFiles.size(); i++) { channelFiles[i]->open(); sourceChannels.emplace_back(channelFiles[i]->readAll()); QByteArray &res = sourceChannels.back(); channelFiles[i]->close(); if (dataSize == 0) { dataSize = res.size(); } if (res.isEmpty() || res.size() != dataSize) { // Something went wrong, abort m_errorMessage.append(i18n("Error reading audio thumbnail created with FFMPEG\n")); return false; } rawChannels.emplace_back((const qint16 *)res.constData()); } int progress = 0; std::vector channelsData; double offset = (double)dataSize / (2.0 * m_lengthInFrames); int intraOffset = 1; if (offset > 1000) { intraOffset = offset / 60; } else if (offset > 250) { intraOffset = offset / 10; } double factor = 800.0 / 32768; for (int i = 0; i < m_lengthInFrames; i++) { channelsData.resize((size_t)rawChannels.size()); std::fill(channelsData.begin(), channelsData.end(), 0); int pos = (int)(i * offset); int steps = 0; for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) { steps++; for (size_t k = 0; k < rawChannels.size(); k++) { channelsData[k] += abs(rawChannels[k][pos + j]); } } for (size_t k = 0; k < channelsData.size(); k++) { if (steps != 0) { channelsData[k] /= steps; } - m_audioLevels << (int) (channelsData[k] * factor); + m_audioLevels << (int)(channelsData[k] * factor); } int p = 80 + (i * 20 / m_lengthInFrames); if (p != progress) { emit jobProgress(p); progress = p; } } m_done = true; return true; } QString err = m_ffmpegProcess->readAllStandardError(); delete m_ffmpegProcess; m_errorMessage += err; m_errorMessage.append(i18n("Failed to create FFmpeg audio thumbnails, we now try to use MLT")); return false; } void AudioThumbJob::updateFfmpegProgress() { QString result = m_ffmpegProcess->readAllStandardOutput(); const QStringList lines = result.split(QLatin1Char('\n')); for (const QString &data : lines) { if (data.startsWith(QStringLiteral("out_time_ms"))) { double ms = data.section(QLatin1Char('='), 1).toDouble(); emit jobProgress((int)(ms / m_binClip->duration().ms() / 10)); } else { m_logDetails += data + QStringLiteral("\n"); } } } bool AudioThumbJob::startJob() { if (m_done) { return true; } m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_binClip->audioChannels() == 0 || m_binClip->audioThumbCreated()) { // nothing to do m_done = true; m_successful = true; return true; } m_prod = m_binClip->originalProducer(); m_frequency = m_binClip->audioInfo()->samplingRate(); m_frequency = m_frequency <= 0 ? 48000 : m_frequency; m_channels = m_binClip->audioInfo()->channels(); m_channels = m_channels <= 0 ? 2 : m_channels; m_lengthInFrames = m_prod->get_length(); m_audioStream = m_binClip->audioInfo()->ffmpeg_audio_index(); if ((m_prod == nullptr) || !m_prod->is_valid()) { m_done = true; m_successful = false; return false; } m_cachePath = m_binClip->getAudioThumbPath(); // checking for cached thumbs QImage image(m_cachePath); if (!image.isNull()) { // convert cached image int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p = image.pixel(i / m_channels, i % m_channels); m_audioLevels << qRed(p); m_audioLevels << qGreen(p); m_audioLevels << qBlue(p); m_audioLevels << qAlpha(p); } } if (!m_audioLevels.isEmpty()) { m_done = true; m_successful = true; return true; } bool ok = m_binClip->clipType() == ClipType::Playlist ? false : computeWithFFMPEG(); ok = ok ? ok : computeWithMlt(); Q_ASSERT(ok == m_done); if (ok && m_done && !m_audioLevels.isEmpty()) { // Put into an image for caching. int count = m_audioLevels.size(); image = QImage((int)lrint((count + 3) / 4.0 / m_channels), m_channels, QImage::Format_ARGB32); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p; if ((4 * i + 3) < count) { p = qRgba(m_audioLevels.at(4 * i).toInt(), m_audioLevels.at(4 * i + 1).toInt(), m_audioLevels.at(4 * i + 2).toInt(), m_audioLevels.at(4 * i + 3).toInt()); } else { int last = m_audioLevels.last().toInt(); int r = (4 * i + 0) < count ? m_audioLevels.at(4 * i + 0).toInt() : last; int g = (4 * i + 1) < count ? m_audioLevels.at(4 * i + 1).toInt() : last; int b = (4 * i + 2) < count ? m_audioLevels.at(4 * i + 2).toInt() : last; int a = last; p = qRgba(r, g, b, a); } image.setPixel(i / m_channels, i % m_channels, p); } image.save(m_cachePath); m_successful = true; return true; } m_done = true; m_successful = false; return false; } -bool AudioThumbJob::commitResult(Fun &undo, Fun &redo) +bool AudioThumbJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } QVariantList old = m_binClip->audioFrameCache; // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [ clip = m_binClip, audio = std::move(m_audioLevels) ]() { clip->updateAudioThumbnail(audio); return true; }; auto reverse = [ clip = m_binClip, audio = std::move(old) ]() { clip->updateAudioThumbnail(audio); return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/audiothumbjob.hpp b/src/jobs/audiothumbjob.hpp index a3f8d8fdb..41b24e14d 100644 --- a/src/jobs/audiothumbjob.hpp +++ b/src/jobs/audiothumbjob.hpp @@ -1,73 +1,73 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #pragma once #include "abstractclipjob.h" #include /* @brief This class represents the job that corresponds to computing the audio thumb of a clip (waveform) */ class ProjectClip; namespace Mlt { class Producer; } class AudioThumbJob : public AbstractClipJob { Q_OBJECT public: /* @brief Extract a thumb for given clip. @param frameNumber is the frame to extract. Leave to -1 for default @param persistent: if true, we will use the persistent cache (for query and saving) */ AudioThumbJob(const QString &binId); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; protected: bool computeWithFFMPEG(); // MLT audio thumbs: slower but safer bool computeWithMlt(); // process the stdout/stderr from ffmpeg void updateFfmpegProgress(); private: std::shared_ptr m_binClip; std::shared_ptr m_prod; QString m_cachePath; bool m_done{false}, m_successful{false}; int m_channels, m_frequency, m_lengthInFrames, m_audioStream; QVariantList m_audioLevels; QProcess *m_ffmpegProcess; }; diff --git a/src/jobs/jobmanager.cpp b/src/jobs/jobmanager.cpp index 71db9fd24..a99eaa6a0 100644 --- a/src/jobs/jobmanager.cpp +++ b/src/jobs/jobmanager.cpp @@ -1,403 +1,406 @@ /* Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 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 "jobmanager.h" #include "bin/abstractprojectitem.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "macros.hpp" #include "undohelper.hpp" #include #include #include int JobManager::m_currentId = 0; JobManager::JobManager(QObject *parent) : QAbstractListModel(parent) , m_lock(QReadWriteLock::Recursive) { } JobManager::~JobManager() { slotCancelJobs(); } int JobManager::getBlockingJobId(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (!m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { return jobId; } } } } return -1; } std::vector JobManager::getPendingJobsIds(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (!m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { result.push_back(jobId); } } } } return result; } std::vector JobManager::getFinishedJobsIds(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (m_jobs.at(jobId)->m_future.isFinished() || m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { result.push_back(jobId); } } } } return result; } void JobManager::discardJobs(const QString &binId, AbstractClipJob::JOBTYPE type) { QWriteLocker locker(&m_lock); if (m_jobsByClip.count(binId) == 0) { return; } for (int jobId : m_jobsByClip.at(binId)) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { m_jobs.at(jobId)->m_future.cancel(); } } } bool JobManager::hasPendingJob(const QString &clipId, AbstractClipJob::JOBTYPE type, int *foundId) { READ_LOCK(); if (m_jobsByClip.count(clipId) > 0) { for (int jobId : m_jobsByClip.at(clipId)) { if ((type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) && !m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (foundId) { *foundId = jobId; } return true; } } if (foundId) { *foundId = -1; } } return false; } void JobManager::updateJobCount() { READ_LOCK(); int count = 0; for (const auto &j : m_jobs) { if (!j.second->m_future.isFinished() && !j.second->m_future.isCanceled()) { count++; /*for (int i = 0; i < j.second->m_future.future().resultCount(); ++i) { if (j.second->m_future.future().isResultReadyAt(i)) { count++; } }*/ } } // Set jobs count emit jobCount(count); } /* void JobManager::prepareJobs(const QList &clips, double fps, AbstractClipJob::JOBTYPE jobType, const QStringList ¶ms) { // TODO filter clips QList matching = filterClips(clips, jobType, params); if (matching.isEmpty()) { m_bin->doDisplayMessage(i18n("No valid clip to process"), KMessageWidget::Information); return; } QHash jobs; if (jobType == AbstractClipJob::TRANSCODEJOB) { jobs = CutClipJob::prepareTranscodeJob(fps, matching, params); } else if (jobType == AbstractClipJob::CUTJOB) { ProjectClip *clip = matching.constFirst(); double originalFps = clip->getOriginalFps(); jobs = CutClipJob::prepareCutClipJob(fps, originalFps, clip); } else if (jobType == AbstractClipJob::ANALYSECLIPJOB) { jobs = CutClipJob::prepareAnalyseJob(fps, matching, params); } else if (jobType == AbstractClipJob::FILTERCLIPJOB) { jobs = FilterJob::prepareJob(matching, params); } else if (jobType == AbstractClipJob::PROXYJOB) { jobs = ProxyJob::prepareJob(m_bin, matching); } if (!jobs.isEmpty()) { QHashIterator i(jobs); while (i.hasNext()) { i.next(); launchJob(i.key(), i.value(), false); } slotCheckJobProcess(); } } */ void JobManager::slotDiscardClipJobs(const QString &binId) { QWriteLocker locker(&m_lock); if (m_jobsByClip.count(binId) > 0) { for (int jobId : m_jobsByClip.at(binId)) { Q_ASSERT(m_jobs.count(jobId) > 0); m_jobs[jobId]->m_future.cancel(); } } } void JobManager::slotCancelPendingJobs() { QWriteLocker locker(&m_lock); for (const auto &j : m_jobs) { if (!j.second->m_future.isStarted()) { j.second->m_future.cancel(); } } } void JobManager::slotCancelJobs() { QWriteLocker locker(&m_lock); for (const auto &j : m_jobs) { j.second->m_future.cancel(); } } void JobManager::createJob(std::shared_ptr job) { /* // This thread wait mechanism was broken and caused a race condition locking the application // so I switched to a simpler model bool ok = false; // wait for parents to finish while (!ok) { ok = true; for (int p : parents) { if (!m_jobs[p]->m_completionMutex.tryLock()) { ok = false; qDebug()<<"********\nWAITING FOR JOB COMPLETION MUTEX!!: "<m_id<<" : "<m_id<<"="<m_type; break; } else { qDebug()<<">>>>>>>>>>\nJOB COMPLETION MUTEX DONE: "<m_id; m_jobs[p]->m_completionMutex.unlock(); } } if (!ok) { QThread::msleep(10); } }*/ // connect progress signals QReadLocker locker(&m_lock); for (const auto &it : job->m_indices) { size_t i = it.second; auto binId = it.first; connect(job->m_job[i].get(), &AbstractClipJob::jobProgress, [job, i, binId](int p) { job->m_progress[i] = std::max(job->m_progress[i], p); pCore->projectItemModel()->onItemUpdated(binId, AbstractProjectItem::JobProgress); }); } connect(&job->m_future, &QFutureWatcher::started, this, &JobManager::updateJobCount); connect(&job->m_future, &QFutureWatcher::finished, [ this, id = job->m_id ]() { slotManageFinishedJob(id); }); connect(&job->m_future, &QFutureWatcher::canceled, [ this, id = job->m_id ]() { slotManageCanceledJob(id); }); job->m_actualFuture = QtConcurrent::mapped(job->m_job, AbstractClipJob::execute); job->m_future.setFuture(job->m_actualFuture); // In the unlikely event that the job finished before the signal connection was made, we check manually for finish and cancel /*if (job->m_future.isFinished()) { //emit job->m_future.finished(); slotManageFinishedJob(job->m_id); } if (job->m_future.isCanceled()) { //emit job->m_future.canceled(); slotManageCanceledJob(job->m_id); }*/ } void JobManager::slotManageCanceledJob(int id) { QReadLocker locker(&m_lock); Q_ASSERT(m_jobs.count(id) > 0); if (m_jobs[id]->m_processed) return; m_jobs[id]->m_processed = true; m_jobs[id]->m_completionMutex.unlock(); // send notification to refresh view for (const auto &it : m_jobs[id]->m_indices) { pCore->projectItemModel()->onItemUpdated(it.first, AbstractProjectItem::JobStatus); } // TODO: delete child jobs updateJobCount(); } void JobManager::slotManageFinishedJob(int id) { qDebug() << "################### JOB finished" << id; QReadLocker locker(&m_lock); Q_ASSERT(m_jobs.count(id) > 0); if (m_jobs[id]->m_processed) return; // send notification to refresh view for (const auto &it : m_jobs[id]->m_indices) { pCore->projectItemModel()->onItemUpdated(it.first, AbstractProjectItem::JobStatus); } bool ok = true; for (bool res : m_jobs[id]->m_future.future()) { ok = ok && res; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; if (!ok) { qDebug() << " * * * ** * * *\nWARNING + + +\nJOB NOT CORRECT FINISH: " << id << "\n------------------------"; // TODO: delete child jobs m_jobs[id]->m_completionMutex.unlock(); locker.unlock(); if (m_jobs.at(id)->m_type == AbstractClipJob::LOADJOB) { // loading failed, remove clip for (const auto &it : m_jobs[id]->m_indices) { std::shared_ptr item = pCore->projectItemModel()->getItemByBinId(it.first); if (item && item->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = std::static_pointer_cast(item); if (!clipItem->isReady()) { // We were trying to load a new clip, delete it - pCore->projectItemModel()->requestBinClipDeletion(item, undo, redo); + pCore->projectItemModel()->requestBinClipDeletion(item, undo, redo, list); } } } } updateJobCount(); + ModelUpdater::applyUpdates(undo, redo, list); return; } // unlock mutex to allow further processing // TODO: the lock mechanism should handle this better! locker.unlock(); for (const auto &j : m_jobs[id]->m_job) { - ok = ok && j->commitResult(undo, redo); + ok = ok && j->commitResult(undo, redo, list); } m_jobs[id]->m_processed = true; if (!ok) { qDebug() << "ERROR: Job " << id << " failed"; m_jobs[id]->m_failed = true; } m_jobs[id]->m_completionMutex.unlock(); if (ok && !m_jobs[id]->m_undoString.isEmpty()) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, m_jobs[id]->m_undoString); } if (m_jobsByParents.count(id) > 0) { std::vector children = m_jobsByParents[id]; for (int cid : children) { QtConcurrent::run(this, &JobManager::createJob, m_jobs[cid]); } m_jobsByParents.erase(id); } updateJobCount(); } AbstractClipJob::JOBTYPE JobManager::getJobType(int jobId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); return m_jobs.at(jobId)->m_type; } JobManagerStatus JobManager::getJobStatus(int jobId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); if (job->m_future.isFinished()) { return JobManagerStatus::Finished; } if (job->m_future.isCanceled()) { return JobManagerStatus::Canceled; } if (job->m_future.isRunning()) { return JobManagerStatus::Running; } return JobManagerStatus::Pending; } int JobManager::getJobProgressForClip(int jobId, const QString &binId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); Q_ASSERT(job->m_indices.count(binId) > 0); size_t ind = job->m_indices.at(binId); return job->m_progress[ind]; } QString JobManager::getJobMessageForClip(int jobId, const QString &binId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); Q_ASSERT(job->m_indices.count(binId) > 0); size_t ind = job->m_indices.at(binId); return job->m_job[ind]->getErrorMessage(); } QVariant JobManager::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } int row = index.row(); if (row >= int(m_jobs.size()) || row < 0) { return QVariant(); } auto it = m_jobs.begin(); std::advance(it, row); switch (role) { case Qt::DisplayRole: return QVariant(it->second->m_job.front()->getDescription()); break; } return QVariant(); } int JobManager::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return int(m_jobs.size()); } diff --git a/src/jobs/loadjob.cpp b/src/jobs/loadjob.cpp index 3460578d5..ab6aed1be 100644 --- a/src/jobs/loadjob.cpp +++ b/src/jobs/loadjob.cpp @@ -1,550 +1,552 @@ /*************************************************************************** * 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 "loadjob.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "mltcontroller/clip.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/slideshowclip.h" #include "xml/xml.hpp" #include #include #include #include #include LoadJob::LoadJob(const QString &binId, const QDomElement &xml) : AbstractClipJob(LOADJOB, binId) , m_xml(xml) { } const QString LoadJob::getDescription() const { return i18n("Loading clip %1", m_clipId); } namespace { ClipType::ProducerType getTypeForService(const QString &id, const QString &path) { if (id.isEmpty()) { QString ext = path.section(QLatin1Char('.'), -1); if (ext == QLatin1String("mlt") || ext == QLatin1String("kdenlive")) { return ClipType::Playlist; } return ClipType::Unknown; } if (id == QLatin1String("color") || id == QLatin1String("colour")) { return ClipType::Color; } if (id == QLatin1String("kdenlivetitle")) { return ClipType::Text; } if (id == QLatin1String("qtext")) { return ClipType::QText; } if (id == QLatin1String("xml") || id == QLatin1String("consumer")) { return ClipType::Playlist; } if (id == QLatin1String("webvfx")) { return ClipType::WebVfx; } return ClipType::Unknown; } // Read the properties of the xml and pass them to the producer. Note that some properties like resource are ignored void processProducerProperties(std::shared_ptr prod, const QDomElement &xml) { // TODO: there is some duplication with clipcontroller > updateproducer that also copies properties QString value; QStringList internalProperties; internalProperties << QStringLiteral("bypassDuplicate") << QStringLiteral("resource") << QStringLiteral("mlt_service") << QStringLiteral("audio_index") << QStringLiteral("video_index") << QStringLiteral("mlt_type"); QDomNodeList props; if (xml.tagName() == QLatin1String("producer")) { props = xml.childNodes(); } else { props = xml.firstChildElement(QStringLiteral("producer")).childNodes(); } for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().tagName() != QStringLiteral("property")) { continue; } QString propertyName = props.at(i).toElement().attribute(QStringLiteral("name")); if (!internalProperties.contains(propertyName) && !propertyName.startsWith(QLatin1Char('_'))) { value = props.at(i).firstChild().nodeValue(); if (propertyName.startsWith(QLatin1String("kdenlive-force."))) { // this is a special forced property, pass it propertyName.remove(0, 15); } prod->set(propertyName.toUtf8().constData(), value.toUtf8().constData()); } } } } // namespace // static std::shared_ptr LoadJob::loadResource(QString &resource, const QString &type) { if (!resource.startsWith(type)) { resource.prepend(type); } return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } std::shared_ptr LoadJob::loadPlaylist(QString &resource) { std::unique_ptr xmlProfile(new Mlt::Profile()); xmlProfile->set_explicit(0); std::unique_ptr producer(new Mlt::Producer(*xmlProfile, "xml", resource.toUtf8().constData())); if (!producer->is_valid()) { qDebug() << "////// ERROR, CANNOT LOAD SELECTED PLAYLIST: " << resource; return nullptr; } if (pCore->getCurrentProfile()->isCompatible(xmlProfile.get())) { // We can use the "xml" producer since profile is the same (using it with different profiles corrupts the project. // Beware that "consumer" currently crashes on audio mixes! resource.prepend(QStringLiteral("xml:")); } else { // This is currently crashing so I guess we'd better reject it for now qDebug() << "////// ERROR, INCOMPATIBLE PROFILE: " << resource; return nullptr; // path.prepend(QStringLiteral("consumer:")); } pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } void LoadJob::checkProfile() { // Check if clip profile matches QString service = m_producer->get("mlt_service"); // Check for image producer if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { // This is an image, create profile from image size int width = m_producer->get_int("meta.media.width"); int height = m_producer->get_int("meta.media.height"); if (width > 100 && height > 100) { std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); projectProfile->m_width = width; projectProfile->m_height = height; projectProfile->m_sample_aspect_num = 1; projectProfile->m_sample_aspect_den = 1; projectProfile->m_display_aspect_num = width; projectProfile->m_display_aspect_den = height; projectProfile->m_description.clear(); pCore->currentDoc()->switchProfile(projectProfile, m_clipId, m_xml); } else { // Very small image, we probably don't want to use this as profile } } else if (service.contains(QStringLiteral("avformat"))) { std::unique_ptr blankProfile(new Mlt::Profile()); blankProfile->set_explicit(0); blankProfile->from_producer(*m_producer); std::unique_ptr clipProfile(new ProfileParam(blankProfile.get())); std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); clipProfile->adjustWidth(); if (*clipProfile.get() == *projectProfile.get()) { if (KdenliveSettings::default_profile().isEmpty()) { // Confirm default project format KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); } } else { // Profiles do not match, propose profile adjustment pCore->currentDoc()->switchProfile(clipProfile, m_clipId, m_xml); } } } void LoadJob::processSlideShow() { int ttl = EffectsList::property(m_xml, QStringLiteral("ttl")).toInt(); QString anim = EffectsList::property(m_xml, QStringLiteral("animation")); if (!anim.isEmpty()) { auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "affine"); if ((filter != nullptr) && filter->is_valid()) { int cycle = ttl; QString geometry = SlideshowClip::animationToGeometry(anim, cycle); if (!geometry.isEmpty()) { if (anim.contains(QStringLiteral("low-pass"))) { auto *blur = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "boxblur"); if ((blur != nullptr) && blur->is_valid()) { m_producer->attach(*blur); } } filter->set("transition.geometry", geometry.toUtf8().data()); filter->set("transition.cycle", cycle); m_producer->attach(*filter); } } } QString fade = EffectsList::property(m_xml, QStringLiteral("fade")); if (fade == QLatin1String("1")) { // user wants a fade effect to slideshow auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "luma"); if ((filter != nullptr) && filter->is_valid()) { if (ttl != 0) { filter->set("cycle", ttl); } QString luma_duration = EffectsList::property(m_xml, QStringLiteral("luma_duration")); QString luma_file = EffectsList::property(m_xml, QStringLiteral("luma_file")); if (!luma_duration.isEmpty()) { filter->set("duration", luma_duration.toInt()); } if (!luma_file.isEmpty()) { filter->set("luma.resource", luma_file.toUtf8().constData()); QString softness = EffectsList::property(m_xml, QStringLiteral("softness")); if (!softness.isEmpty()) { int soft = softness.toInt(); filter->set("luma.softness", (double)soft / 100.0); } } m_producer->attach(*filter); } } QString crop = EffectsList::property(m_xml, QStringLiteral("crop")); if (crop == QLatin1String("1")) { // user wants to center crop the slides auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "crop"); if ((filter != nullptr) && filter->is_valid()) { filter->set("center", 1); m_producer->attach(*filter); } } } bool LoadJob::startJob() { if (m_done) { return true; } m_resource = Xml::getXmlProperty(m_xml, QStringLiteral("resource")); ClipType::ProducerType type = static_cast(m_xml.attribute(QStringLiteral("type")).toInt()); if (type == ClipType::Unknown) { type = getTypeForService(Xml::getXmlProperty(m_xml, QStringLiteral("mlt_service")), m_resource); } switch (type) { case ClipType::Color: m_producer = loadResource(m_resource, QStringLiteral("color:")); break; case ClipType::Text: case ClipType::TextTemplate: m_producer = loadResource(m_resource, QStringLiteral("kdenlivetitle:")); break; case ClipType::QText: m_producer = loadResource(m_resource, QStringLiteral("qtext:")); break; case ClipType::Playlist: m_producer = loadPlaylist(m_resource); break; case ClipType::SlideShow: default: m_producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_resource.toUtf8().constData()); break; } if (!m_producer || m_producer->is_blank() || !m_producer->is_valid()) { qCDebug(KDENLIVE_LOG) << " / / / / / / / / ERROR / / / / // CANNOT LOAD PRODUCER: " << m_resource; m_done = true; m_successful = false; if (m_producer) { m_producer.reset(); } QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, i18n("Cannot open file %1", m_resource)), Q_ARG(int, (int)KMessageWidget::Warning)); m_errorMessage.append(i18n("ERROR: Could not load clip %1: producer is invalid", m_resource)); return false; } processProducerProperties(m_producer, m_xml); QString clipName = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:clipname")); if (clipName.isEmpty()) { clipName = QFileInfo(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:originalurl"))).fileName(); } m_producer->set("kdenlive:clipname", clipName.toUtf8().constData()); QString groupId = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:folderid")); if (!groupId.isEmpty()) { m_producer->set("kdenlive:folderid", groupId.toUtf8().constData()); } int clipOut = 0, duration = 0; if (m_xml.hasAttribute(QStringLiteral("out"))) { clipOut = m_xml.attribute(QStringLiteral("out")).toInt(); } // setup length here as otherwise default length (currently 15000 frames in MLT) will be taken even if outpoint is larger if (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || type == ClipType::SlideShow) { int length; if (m_xml.hasAttribute(QStringLiteral("length"))) { length = m_xml.attribute(QStringLiteral("length")).toInt(); clipOut = qMax(1, length - 1); } else { length = Xml::getXmlProperty(m_xml, QStringLiteral("length")).toInt(); clipOut -= m_xml.attribute(QStringLiteral("in")).toInt(); if (length < clipOut) { length = clipOut == 1 ? 1 : clipOut + 1; } } // Pass duration if it was forced if (m_xml.hasAttribute(QStringLiteral("duration"))) { duration = m_xml.attribute(QStringLiteral("duration")).toInt(); if (length < duration) { length = duration; if (clipOut > 0) { clipOut = length - 1; } } } if (duration == 0) { duration = length; } m_producer->set("length", length); int kdenlive_duration = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:duration")).toInt(); m_producer->set("kdenlive:duration", kdenlive_duration > 0 ? kdenlive_duration : length); } if (clipOut > 0) { m_producer->set_in_and_out(m_xml.attribute(QStringLiteral("in")).toInt(), clipOut); } if (m_xml.hasAttribute(QStringLiteral("templatetext"))) { m_producer->set("templatetext", m_xml.attribute(QStringLiteral("templatetext")).toUtf8().constData()); } duration = duration > 0 ? duration : m_producer->get_playtime(); if (type == ClipType::SlideShow) { processSlideShow(); } int vindex = -1; double fps = -1; const QString mltService = m_producer->get("mlt_service"); if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { // MLT playlist, create producer with blank profile to get real profile info QString tmpPath = m_resource; if (tmpPath.startsWith(QLatin1String("consumer:"))) { tmpPath = "xml:" + tmpPath.section(QLatin1Char(':'), 1); } Mlt::Profile original_profile; std::unique_ptr tmpProd(new Mlt::Producer(original_profile, nullptr, tmpPath.toUtf8().constData())); original_profile.set_explicit(1); double originalFps = original_profile.fps(); fps = originalFps; if (originalFps > 0 && !qFuzzyCompare(originalFps, pCore->getCurrentFps())) { int originalLength = tmpProd->get_length(); int fixedLength = (int)(originalLength * pCore->getCurrentFps() / originalFps); m_producer->set("length", fixedLength); m_producer->set("out", fixedLength - 1); } } else if (mltService == QLatin1String("avformat")) { // check if there are multiple streams vindex = m_producer->get_int("video_index"); // List streams int streams = m_producer->get_int("meta.media.nb_streams"); m_audio_list.clear(); m_video_list.clear(); for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString stype = m_producer->get(propertyName.data()); if (stype == QLatin1String("audio")) { m_audio_list.append(i); } else if (stype == QLatin1String("video")) { m_video_list.append(i); } } if (vindex > -1) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); fps = m_producer->get_double(property); } if (fps <= 0) { if (m_producer->get_double("meta.media.frame_rate_den") > 0) { fps = m_producer->get_double("meta.media.frame_rate_num") / m_producer->get_double("meta.media.frame_rate_den"); } else { fps = m_producer->get_double("source_fps"); } } } if (fps <= 0 && type == ClipType::Unknown) { // something wrong, maybe audio file with embedded image QMimeDatabase db; QString mime = db.mimeTypeForFile(m_resource).name(); if (mime.startsWith(QLatin1String("audio"))) { m_producer->set("video_index", -1); vindex = -1; } } m_done = m_successful = true; return true; } void LoadJob::processMultiStream() { auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We retrieve the folder containing our clip, because we will set the other streams in the same auto parent = pCore->projectItemModel()->getRootFolder()->clipId(); if (auto ptr = m_binClip->parentItem().lock()) { parent = std::static_pointer_cast(ptr)->clipId(); } else { qDebug() << "Warning, something went wrong while accessing parent of bin clip"; } // This helper lambda request addition of a given stream - auto addStream = [ this, parentId = std::move(parent) ](int vindex, int aindex, Fun &undo, Fun &redo) + auto addStream = [ this, parentId = std::move(parent) ](int vindex, int aindex, Fun &undo, Fun &redo, Updates &list) { auto clone = Clip::clone(m_producer); clone->set("video_index", vindex); clone->set("audio_index", aindex); QString id; pCore->projectItemModel()->requestAddBinClip(id, clone, parentId, undo, redo); }; Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; if (KdenliveSettings::automultistreams()) { for (int i = 1; i < m_video_list.count(); ++i) { int vindex = m_video_list.at(i); int aindex = 0; if (i <= m_audio_list.count() - 1) { aindex = m_audio_list.at(i); } - addStream(vindex, aindex, undo, redo); + addStream(vindex, aindex, undo, redo, list); } return; } int width = 60.0 * pCore->getCurrentDar(); if (width % 2 == 1) { width++; } QScopedPointer dialog(new QDialog(qApp->activeWindow())); dialog->setWindowTitle(QStringLiteral("Multi Stream Clip")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(dialog.data()); auto *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); okButton->setText(i18n("Import selected clips")); QLabel *lab1 = new QLabel(i18n("Additional streams for clip\n %1", m_resource), mainWidget); mainLayout->addWidget(lab1); QList groupList; QList comboList; // We start loading the list at 1, video index 0 should already be loaded for (int j = 1; j < m_video_list.count(); ++j) { m_producer->set("video_index", m_video_list.at(j)); // TODO this keyframe should be cached QImage thumb = KThumb::getFrame(m_producer.get(), 0, width, 60); QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", m_video_list.at(j)), mainWidget); mainLayout->addWidget(streamFrame); streamFrame->setProperty("vindex", m_video_list.at(j)); groupList << streamFrame; streamFrame->setCheckable(true); streamFrame->setChecked(true); auto *vh = new QVBoxLayout(streamFrame); QLabel *iconLabel = new QLabel(mainWidget); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(QPixmap::fromImage(thumb)); vh->addWidget(iconLabel); if (m_audio_list.count() > 1) { auto *cb = new KComboBox(mainWidget); mainLayout->addWidget(cb); for (int k = 0; k < m_audio_list.count(); ++k) { cb->addItem(i18n("Audio stream %1", m_audio_list.at(k)), m_audio_list.at(k)); } comboList << cb; cb->setCurrentIndex(qMin(j, m_audio_list.count() - 1)); vh->addWidget(cb); } mainLayout->addWidget(streamFrame); } mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { // import selected streams for (int i = 0; i < groupList.count(); ++i) { if (groupList.at(i)->isChecked()) { int vindex = groupList.at(i)->property("vindex").toInt(); int ax = qMin(i, comboList.size() - 1); int aindex = -1; if (ax >= 0) { // only check audio index if we have several audio streams aindex = comboList.at(ax)->itemData(comboList.at(ax)->currentIndex()).toInt(); } - addStream(vindex, aindex, undo, redo); + addStream(vindex, aindex, undo, redo, list); } } } + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Add additional streams for clip")); } -bool LoadJob::commitResult(Fun &undo, Fun &redo) +bool LoadJob::commitResult(Fun &undo, Fun &redo, Updates &list) { qDebug() << "################### loadjob COMMIT"; Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (!m_successful) { // TODO: Deleting cannot happen at this stage or we endup in a mutex lock - pCore->projectItemModel()->requestBinClipDeletion(m_binClip, undo, redo); + pCore->projectItemModel()->requestBinClipDeletion(m_binClip, undo, redo, list); return false; } if (m_xml.hasAttribute(QStringLiteral("_checkProfile")) && m_producer->get_int("video_index") > -1) { checkProfile(); } if (m_video_list.size() > 1) { processMultiStream(); } // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [ clip = m_binClip, prod = std::move(m_producer) ]() { clip->setProducer(prod, true); return true; }; auto reverse = []() { // This is probably not invertible, leave as is. return true; }; bool ok = operation(); if (ok) { if (pCore->projectItemModel()->clipsCount() == 2) { // Always select first added clip pCore->selectBinClip(m_clipId); } UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/loadjob.hpp b/src/jobs/loadjob.hpp index 2dcd43400..5859df297 100644 --- a/src/jobs/loadjob.hpp +++ b/src/jobs/loadjob.hpp @@ -1,80 +1,80 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #pragma once #include "abstractclipjob.h" #include #include /* @brief This class represents the job that corresponds to loading a clip from xml */ class ProjectClip; namespace Mlt { class Producer; } class LoadJob : public AbstractClipJob { Q_OBJECT public: /* @brief Extract a thumb for given clip. @param frameNumber is the frame to extract. Leave to -1 for default @param persistent: if true, we will use the persistent cache (for query and saving) */ LoadJob(const QString &binId, const QDomElement &xml); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; protected: // helper to load some kind of resources such as color. This will modify resource if needs be (for eg., in the case of color, it will prepend "color:" if // needed) static std::shared_ptr loadResource(QString &resource, const QString &type); std::shared_ptr loadPlaylist(QString &resource); // Do some checks on the profile void checkProfile(); // Create the required filter for a slideshow void processSlideShow(); // This should be called from commitResult (that is, from the GUI thread) to deal with multi stream videos void processMultiStream(); private: QDomElement m_xml; bool m_done{false}, m_successful{false}; std::shared_ptr m_producer; QList m_audio_list, m_video_list; QString m_resource; }; diff --git a/src/jobs/proxyclipjob.cpp b/src/jobs/proxyclipjob.cpp index e97cb03cd..7ae9efe97 100644 --- a/src/jobs/proxyclipjob.cpp +++ b/src/jobs/proxyclipjob.cpp @@ -1,342 +1,342 @@ /*************************************************************************** * * * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "proxyclipjob.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "macros.hpp" #include #include #include ProxyJob::ProxyJob(const QString &binId) : AbstractClipJob(PROXYJOB, binId) , m_jobDuration(0) , m_isFfmpegJob(true) , m_jobProcess(nullptr) , m_done(false) { } const QString ProxyJob::getDescription() const { return i18n("Creating proxy %1", m_clipId); } bool ProxyJob::startJob() { auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (QFile::exists(dest) && QFileInfo(dest).size() > 0) { // Proxy clip already created m_done = true; return true; } ClipType::ProducerType type = binClip->clipType(); bool result; QString source = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl")); int exif = binClip->getProducerIntProperty(QStringLiteral("_exif_orientation")); if (type == ClipType::Playlist || type == ClipType::SlideShow) { // change FFmpeg params to MLT format m_isFfmpegJob = false; QStringList mltParameters; QTemporaryFile *playlist = nullptr; // set clip origin if (type == ClipType::Playlist) { // Special case: playlists use the special 'consumer' producer to support resizing source.prepend(QStringLiteral("consumer:")); } else { // create temporary playlist to generate proxy // we save a temporary .mlt clip for rendering QDomDocument doc; QDomElement xml = binClip->toXml(doc, false); playlist = new QTemporaryFile(); playlist->setFileTemplate(playlist->fileTemplate() + QStringLiteral(".mlt")); if (playlist->open()) { source = playlist->fileName(); QTextStream out(playlist); out << doc.toString(); playlist->close(); } } mltParameters << source; // set destination mltParameters << QStringLiteral("-consumer") << QStringLiteral("avformat:") + dest; QString parameter = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified(); QStringList params = parameter.split(QLatin1Char('-'), QString::SkipEmptyParts); double display_ratio; if (source.startsWith(QLatin1String("consumer:"))) { display_ratio = KdenliveDoc::getDisplayRatio(source.section(QLatin1Char(':'), 1)); } else { display_ratio = KdenliveDoc::getDisplayRatio(source); } if (display_ratio < 1e-6) { display_ratio = 1; } bool skipNext = false; for (const QString &s : params) { QString t = s.simplified(); if (skipNext) { skipNext = false; continue; } if (t.count(QLatin1Char(' ')) == 0) { t.append(QLatin1String("=1")); } else if (t.startsWith(QLatin1String("vf "))) { skipNext = true; bool ok = false; int width = t.section(QLatin1Char('='), 1, 1).section(QLatin1Char(':'), 0, 0).toInt(&ok); if (!ok) { width = 640; } int height = width / display_ratio; // Make sure we get an even height height += height % 2; mltParameters << QStringLiteral("s=%1x%2").arg(width).arg(height); if (t.contains(QStringLiteral("yadif"))) { mltParameters << QStringLiteral("progressive=1"); } continue; } else { t.replace(QLatin1Char(' '), QLatin1String("=")); } mltParameters << t; } mltParameters.append(QStringLiteral("real_time=-%1").arg(KdenliveSettings::mltthreads())); // TODO: currently, when rendering an xml file through melt, the display ration is lost, so we enforce it manualy mltParameters << QStringLiteral("aspect=") + QLocale().toString(display_ratio); // Ask for progress reporting mltParameters << QStringLiteral("progress=1"); m_jobProcess = new QProcess; m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); connect(m_jobProcess, &QProcess::readyReadStandardOutput, this, &ProxyJob::processLogInfo); m_jobProcess->start(KdenliveSettings::rendererpath(), mltParameters); m_jobProcess->waitForFinished(-1); result = m_jobProcess->exitStatus() == QProcess::NormalExit; delete playlist; } else if (type == ClipType::Image) { m_isFfmpegJob = false; // Image proxy QImage i(source); if (i.isNull()) { m_done = false; m_errorMessage.append(i18n("Cannot load image %1.", source)); return false; } QImage proxy; // Images are scaled to profile size. // TODO: Make it be configurable? if (i.width() > i.height()) { proxy = i.scaledToWidth(KdenliveSettings::proxyimagesize()); } else { proxy = i.scaledToHeight(KdenliveSettings::proxyimagesize()); } if (exif > 1) { // Rotate image according to exif data QImage processed; QMatrix matrix; switch (exif) { case 2: matrix.scale(-1, 1); break; case 3: matrix.rotate(180); break; case 4: matrix.scale(1, -1); break; case 5: matrix.rotate(270); matrix.scale(-1, 1); break; case 6: matrix.rotate(90); break; case 7: matrix.rotate(90); matrix.scale(-1, 1); break; case 8: matrix.rotate(270); break; } processed = proxy.transformed(matrix); processed.save(dest); } else { proxy.save(dest); } m_done = true; return true; } else { m_isFfmpegJob = true; QStringList parameters; if (KdenliveSettings::ffmpegpath().isEmpty()) { // FFmpeg not detected, cannot process the Job m_errorMessage.prepend(i18n("Failed to create proxy. FFmpeg not found, please set path in Kdenlive's settings Environment")); m_done = true; return false; } const QString proxyParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified(); if (proxyParams.contains(QStringLiteral("-noautorotate"))) { // The noautorotate flag must be passed before input source parameters << QStringLiteral("-noautorotate"); } if (proxyParams.contains(QLatin1String("-i "))) { // we have some pre-filename parameters, filename will be inserted later } else { parameters << QStringLiteral("-i") << source; } QString params = proxyParams; for (const QString &s : params.split(QLatin1Char(' '))) { QString t = s.simplified(); if (t != QLatin1String("-noautorotate")) { parameters << t; if (t == QLatin1String("-i")) { parameters << source; } } } // Make sure we don't block when proxy file already exists parameters << QStringLiteral("-y"); parameters << dest; m_jobProcess = new QProcess; m_jobProcess->setProcessChannelMode(QProcess::MergedChannels); connect(m_jobProcess, &QProcess::readyReadStandardOutput, this, &ProxyJob::processLogInfo); m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly); m_jobProcess->waitForFinished(-1); result = m_jobProcess->exitStatus() == QProcess::NormalExit; } // remove temporary playlist if it exists if (result) { if (QFileInfo(dest).size() == 0) { QFile::remove(dest); // File was not created m_done = false; m_errorMessage.append(i18n("Failed to create proxy clip.")); } else { m_done = true; } } else { // Proxy process crashed QFile::remove(dest); m_done = false; m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll())); } m_jobProcess->deleteLater(); return result; } void ProxyJob::processLogInfo() { m_buffer.append(QString::fromUtf8(m_jobProcess->readAllStandardOutput())); // reading data from process sometimes returns half a line. To get a correct parsing // we need to store it in a buffer and cut the lines manually int lineFeed = m_buffer.lastIndexOf(QRegExp("[\n\r]")); if (lineFeed == -1) { return; } const QString buffer = m_buffer.left(lineFeed); m_buffer.remove(0, lineFeed + 1); int progress; if (m_isFfmpegJob) { // Parse FFmpeg output if (m_jobDuration == 0) { if (buffer.contains(QLatin1String("Duration:"))) { QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); if (!data.isEmpty()) { QStringList numbers = data.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble()); } } } else if (buffer.contains(QLatin1String("time="))) { QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); if (!time.isEmpty()) { QStringList numbers = time.split(QLatin1Char(':')); if (numbers.size() < 3) { progress = (int)time.toDouble(); if (progress == 0) { return; } } else { progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); } } emit jobProgress((int)(100.0 * progress / m_jobDuration)); } } else { // Parse MLT output if (buffer.contains(QLatin1String("percentage:"))) { progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt(); emit jobProgress(progress); } } } -bool ProxyJob::commitResult(Fun &undo, Fun &redo) +bool ProxyJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); binClip->setProducerProperty(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); return false; } m_resultConsumed = true; auto operation = [clipId = m_clipId]() { auto binClip = pCore->projectItemModel()->getClipByBinID(clipId); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); binClip->setProducerProperty(QStringLiteral("resource"), dest); pCore->bin()->reloadClip(clipId); return true; }; auto reverse = [clipId = m_clipId]() { auto binClip = pCore->projectItemModel()->getClipByBinID(clipId); const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl")); binClip->setProducerProperty(QStringLiteral("resource"), dest); pCore->bin()->reloadClip(clipId); return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; return true; } diff --git a/src/jobs/proxyclipjob.h b/src/jobs/proxyclipjob.h index ce8bae297..3e50d8ee9 100644 --- a/src/jobs/proxyclipjob.h +++ b/src/jobs/proxyclipjob.h @@ -1,54 +1,54 @@ /*************************************************************************** * * * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef PROXYCLIPJOB #define PROXYCLIPJOB #include "abstractclipjob.h" class QTemporaryFile; class Bin; class ProjectClip; class QProcess; class ProxyJob : public AbstractClipJob { Q_OBJECT public: ProxyJob(const QString &binId); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; private slots: void processLogInfo(); private: int m_jobDuration; bool m_isFfmpegJob; QProcess *m_jobProcess; bool m_done; QString m_buffer; }; #endif diff --git a/src/jobs/scenesplitjob.cpp b/src/jobs/scenesplitjob.cpp index 693bd76fd..c4bfb8119 100644 --- a/src/jobs/scenesplitjob.cpp +++ b/src/jobs/scenesplitjob.cpp @@ -1,173 +1,173 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "scenesplitjob.hpp" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include "ui_scenecutdialog_ui.h" #include #include #include SceneSplitJob::SceneSplitJob(const QString &binId, bool subClips, int markersType, int minInterval) : MeltJob(binId, STABILIZEJOB, true, -1, -1) , m_subClips(subClips) , m_markersType(markersType) , m_minInterval(minInterval) { } const QString SceneSplitJob::getDescription() const { return i18n("Scene split"); } void SceneSplitJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "null")); m_consumer->set("all", 1); m_consumer->set("terminate_on_pause", 1); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); // We just want to find scene change, set all methods to the fastests m_consumer->set("rescale", "nearest"); m_consumer->set("deinterlace_method", "onefield"); m_consumer->set("top_field_first", -1); } void SceneSplitJob::configureFilter() { m_filter.reset(new Mlt::Filter(*m_profile.get(), "motion_est")); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage.append(i18n("Cannot create filter motion_est. Cannot split scenes")); return; } m_filter->set("shot_change_list", 0); m_filter->set("denoise", 0); } void SceneSplitJob::configureProfile() { m_profile->set_height(160); m_profile->set_width(m_profile->height() * m_profile->sar()); } // static int SceneSplitJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString) { // Show config dialog QScopedPointer d(new QDialog(QApplication::activeWindow())); Ui::SceneCutDialog_UI ui; ui.setupUi(d.data()); // Set up categories for (size_t i = 0; i < MarkerListModel::markerTypes.size(); ++i) { ui.marker_type->insertItem((int)i, i18n("Category %1", i)); ui.marker_type->setItemData((int)i, MarkerListModel::markerTypes[i], Qt::DecorationRole); } ui.marker_type->setCurrentIndex(KdenliveSettings::default_marker_type()); ui.zone_only->setEnabled(false); // not implemented ui.store_data->setEnabled(false); // not implemented if (d->exec() != QDialog::Accepted) { return -1; } int markersType = ui.add_markers->isChecked() ? ui.marker_type->currentIndex() : -1; bool subclips = ui.cut_scenes->isChecked(); int minInterval = ui.minDuration->value(); return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), subclips, markersType, minInterval); } -bool SceneSplitJob::commitResult(Fun &undo, Fun &redo) +bool SceneSplitJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_UNUSED(undo) Q_UNUSED(redo) Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } QString result = QString::fromLatin1(m_filter->get("shot_change_list")); if (result.isEmpty()) { m_errorMessage.append(i18n("No data returned from clip analysis")); return false; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); QStringList markerData = result.split(QLatin1Char(';')); if (m_markersType >= 0) { // Build json data for markers QJsonArray list; int ix = 1; int lastCut = 0; for (const QString marker : markerData) { int pos = marker.section(QLatin1Char('='), 0, 0).toInt(); if (m_minInterval > 0 && ix > 1 && pos - lastCut < m_minInterval) { continue; } lastCut = pos; QJsonObject currentMarker; currentMarker.insert(QLatin1String("pos"), QJsonValue(pos)); currentMarker.insert(QLatin1String("comment"), QJsonValue(i18n("Scene %1", ix))); currentMarker.insert(QLatin1String("type"), QJsonValue(m_markersType)); list.push_back(currentMarker); ix++; } QJsonDocument json(list); binClip->getMarkerModel()->importFromJson(QString(json.toJson()), true, undo, redo); } if (m_subClips) { // Create zones int ix = 1; int lastCut = 0; QMap zoneData; for (const QString marker : markerData) { int pos = marker.section(QLatin1Char('='), 0, 0).toInt(); if (pos <= lastCut + 1 || pos - lastCut < m_minInterval) { continue; } zoneData.insert(i18n("Scene %1", ix), QString("%1;%2").arg(lastCut).arg(pos - 1)); lastCut = pos; ix++; } if (!zoneData.isEmpty()) { pCore->projectItemModel()->loadSubClips(m_clipId, zoneData, undo, redo); } } qDebug() << "RESULT of the SCENESPLIT filter:" << result; // TODO refac: reimplement add markers and subclips return true; } diff --git a/src/jobs/scenesplitjob.hpp b/src/jobs/scenesplitjob.hpp index f0e692016..4804f9974 100644 --- a/src/jobs/scenesplitjob.hpp +++ b/src/jobs/scenesplitjob.hpp @@ -1,70 +1,70 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class SceneSplitJob * @brief Detects the scenes of a clip using a mlt filter * */ class JobManager; class SceneSplitJob : public MeltJob { Q_OBJECT public: /** @brief Creates a scenesplit job for the given bin clip @param subClips if true, we create a subclip per found scene @param markersType The type of markers that will be created to denote scene. Leave -1 for no markers */ SceneSplitJob(const QString &binId, bool subClips, int markersType = -1, int minInterval = 0); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString); - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure filter void configureFilter() override; // @brief extra configuration of the profile (eg: resize the profile) void configureProfile() override; bool m_subClips; int m_markersType; // @brief minimum scene duration. int m_minInterval; }; diff --git a/src/jobs/speedjob.cpp b/src/jobs/speedjob.cpp index 32614ea21..444e5d315 100644 --- a/src/jobs/speedjob.cpp +++ b/src/jobs/speedjob.cpp @@ -1,139 +1,139 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "speedjob.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include "ui_scenecutdialog_ui.h" #include #include #include #include SpeedJob::SpeedJob(const QString &binId, double speed, const QString &destUrl) : MeltJob(binId, SPEEDJOB, false, -1, -1) , m_speed(speed) , m_destUrl(destUrl) { m_requiresFilter = false; } const QString SpeedJob::getDescription() const { return i18n("Change clip speed"); } void SpeedJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "xml", m_destUrl.toUtf8().constData())); m_consumer->set("terminate_on_pause", 1); m_consumer->set("title", "Speed Change"); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } void SpeedJob::configureProducer() { if (!qFuzzyCompare(m_speed, 1.0)) { QString resource = m_producer->get("resource"); m_producer.reset(new Mlt::Producer(*m_profile.get(), "timewarp", QStringLiteral("%1:%2").arg(m_speed).arg(resource).toUtf8().constData())); } } void SpeedJob::configureFilter() {} // static int SpeedJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString) { // Show config dialog bool ok; int speed = QInputDialog::getInt(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), 100, -100000, 100000, 1, &ok); if (!ok) { return -1; } std::unordered_map destinations; // keys are binIds, values are path to target files for (const auto &binId : binIds) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); // Filter several clips, destination points to a folder QString mltfile = QFileInfo(binClip->url()).absoluteFilePath() + QStringLiteral(".mlt"); destinations[binId] = mltfile; } // Now we have to create the jobs objects. This is trickier than usual, since the parameters are differents for each job (each clip has its own // destination). We have to construct a lambda that does that. auto createFn = [ dest = std::move(destinations), fSpeed = speed / 100.0 ](const QString &id) { return std::make_shared(id, fSpeed, dest.at(id)); }; // We are now all set to create the job. Note that we pass all the parameters directly through the lambda, hence there are no extra parameters to the // function using local_createFn_t = std::function(const QString &)>; return ptr->startJob(binIds, parentId, std::move(undoString), local_createFn_t(std::move(createFn))); } -bool SpeedJob::commitResult(Fun &undo, Fun &redo) +bool SpeedJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We store the stabilized clips in a sub folder with this name const QString folderName(i18n("Speed Change")); QString folderId = QStringLiteral("-1"); bool found = false; // We first try to see if it exists auto containingFolder = std::static_pointer_cast(binClip->parent()); for (int i = 0; i < containingFolder->childCount(); ++i) { auto currentItem = std::static_pointer_cast(containingFolder->child(i)); if (currentItem->itemType() == AbstractProjectItem::FolderItem && currentItem->name() == folderName) { found = true; folderId = currentItem->clipId(); break; } } if (!found) { // if it was not found, we create it pCore->projectItemModel()->requestAddFolder(folderId, folderName, binClip->parent()->clipId(), undo, redo); } auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo); return id != QStringLiteral("-1"); } diff --git a/src/jobs/speedjob.hpp b/src/jobs/speedjob.hpp index d79588850..81c322c05 100644 --- a/src/jobs/speedjob.hpp +++ b/src/jobs/speedjob.hpp @@ -1,67 +1,67 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class SpeedJob * @brief Create a timewarp producer to change speed of a producer * */ class JobManager; class SpeedJob : public MeltJob { Q_OBJECT public: /** @brief Creates a timewarp producer @param speed The speed value */ SpeedJob(const QString &binId, double speed, const QString &destUrl); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString); - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure producer void configureProducer() override; // @brief create and configure filter void configureFilter() override; double m_speed; QString m_destUrl; }; diff --git a/src/jobs/stabilizejob.cpp b/src/jobs/stabilizejob.cpp index 890e63c58..c91d82426 100644 --- a/src/jobs/stabilizejob.cpp +++ b/src/jobs/stabilizejob.cpp @@ -1,158 +1,158 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "stabilizejob.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include #include StabilizeJob::StabilizeJob(const QString &binId, const QString &filterName, const QString &destUrl, const std::unordered_map &filterParams) : MeltJob(binId, STABILIZEJOB, true, -1, -1) , m_filterName(filterName) , m_destUrl(destUrl) , m_filterParams(filterParams) { Q_ASSERT(supportedFilters().count(filterName) > 0); } const QString StabilizeJob::getDescription() const { return i18n("Stabilize clips"); } void StabilizeJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "xml", m_destUrl.toUtf8().constData())); m_consumer->set("all", 1); m_consumer->set("title", "Stabilized"); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } void StabilizeJob::configureFilter() { m_filter.reset(new Mlt::Filter(*m_profile.get(), m_filterName.toUtf8().data())); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage.append(i18n("Cannot create filter %1", m_filterName)); return; } // Process filter params for (const auto &it : m_filterParams) { m_filter->set(it.first.toUtf8().constData(), it.second.toUtf8().constData()); } QString targetFile = m_destUrl + QStringLiteral(".trf"); m_filter->set("filename", targetFile.toUtf8().constData()); } // static std::unordered_set StabilizeJob::supportedFilters() { return {QLatin1String("vidstab"), QLatin1String("videostab2"), QLatin1String("videostab")}; } // static int StabilizeJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, const QString &filterName) { Q_ASSERT(supportedFilters().count(filterName) > 0); if (filterName == QLatin1String("vidstab") || filterName == QLatin1String("videostab2") || filterName == QLatin1String("videostab")) { // vidstab QScopedPointer d(new ClipStabilize(binIds, filterName, 100000)); if (d->exec() == QDialog::Accepted) { std::unordered_map filterParams = d->filterParams(); QString destination = d->destination(); std::unordered_map destinations; // keys are binIds, values are path to target files for (const auto &binId : binIds) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); if (binIds.size() == 1) { // We only have one clip, destination points to the final url destinations[binId] = destination; } else { // Filter several clips, destination points to a folder QString mltfile = destination + QFileInfo(binClip->url()).fileName() + QStringLiteral(".mlt"); destinations[binId] = mltfile; } } // Now we have to create the jobs objects. This is trickier than usual, since the parameters are differents for each job (each clip has its own // destination). We have to construct a lambda that does that. auto createFn = [ dest = std::move(destinations), fName = std::move(filterName), fParams = std::move(filterParams) ](const QString &id) { return std::make_shared(id, fName, dest.at(id), fParams); }; // We are now all set to create the job. Note that we pass all the parameters directly through the lambda, hence there are no extra parameters to // the function using local_createFn_t = std::function(const QString &)>; return ptr->startJob(binIds, parentId, std::move(undoString), local_createFn_t(std::move(createFn))); } } return -1; } -bool StabilizeJob::commitResult(Fun &undo, Fun &redo) +bool StabilizeJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We store the stabilized clips in a sub folder with this name const QString folderName(i18n("Stabilized")); QString folderId = QStringLiteral("-1"); bool found = false; // We first try to see if it exists auto containingFolder = std::static_pointer_cast(binClip->parent()); for (int i = 0; i < containingFolder->childCount(); ++i) { auto currentItem = std::static_pointer_cast(containingFolder->child(i)); if (currentItem->itemType() == AbstractProjectItem::FolderItem && currentItem->name() == folderName) { found = true; folderId = currentItem->clipId(); break; } } if (!found) { // if it was not found, we create it pCore->projectItemModel()->requestAddFolder(folderId, folderName, binClip->parent()->clipId(), undo, redo); } auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo); return id != QStringLiteral("-1"); } diff --git a/src/jobs/stabilizejob.hpp b/src/jobs/stabilizejob.hpp index 5c2120195..a064257f0 100644 --- a/src/jobs/stabilizejob.hpp +++ b/src/jobs/stabilizejob.hpp @@ -1,71 +1,71 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class StabilizeJob * @brief Stabilize a clip using a mlt filter * */ class JobManager; class StabilizeJob : public MeltJob { Q_OBJECT public: /** @brief Creates a stabilize job job for the given bin clip @brief filterName is the name of the actual melt filter to use @brief destUrl is the path to the file we are going to produce @brief filterParams is a map containing the xml parameters of the filter */ StabilizeJob(const QString &binId, const QString &filterName, const QString &destUrl, const std::unordered_map &filterparams); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, const QString &filterName); // Return the list of stabilization filters that we support static std::unordered_set supportedFilters(); - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure filter void configureFilter() override; protected: QString m_filterName; QString m_destUrl; std::unordered_map m_filterParams; }; diff --git a/src/jobs/thumbjob.cpp b/src/jobs/thumbjob.cpp index 6dd2a5ab8..db416fa47 100644 --- a/src/jobs/thumbjob.cpp +++ b/src/jobs/thumbjob.cpp @@ -1,176 +1,176 @@ /*************************************************************************** * 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 "thumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "bin/projectsubclip.h" #include "core.h" #include "doc/kthumb.h" #include "klocalizedstring.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include ThumbJob::ThumbJob(const QString &binId, int imageHeight, int frameNumber, bool persistent, bool reloadAllThumbs) : AbstractClipJob(THUMBJOB, binId) , m_frameNumber(frameNumber) , m_fullWidth(imageHeight * pCore->getCurrentDar() + 0.5) , m_imageHeight(imageHeight) , m_persistent(persistent) , m_reloadAll(reloadAllThumbs) , m_subClip(false) { auto item = pCore->projectItemModel()->getItemByBinId(binId); Q_ASSERT(item->itemType() == AbstractProjectItem::ClipItem || item->itemType() == AbstractProjectItem::SubClipItem); if (item->itemType() == AbstractProjectItem::ClipItem) { m_binClip = pCore->projectItemModel()->getClipByBinID(binId); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { m_subClip = true; m_binClip = pCore->projectItemModel()->getClipByBinID(item->parent()->clipId()); m_frameNumber = std::max(m_frameNumber, std::static_pointer_cast(item)->zone().x()); } } const QString ThumbJob::getDescription() const { return i18n("Extracting thumb at frame %1 from clip %2", m_frameNumber, m_clipId); } bool ThumbJob::startJob() { if (m_done) { return true; } // We reload here, because things may have changed since creation of this job if (m_subClip) { auto item = pCore->projectItemModel()->getItemByBinId(m_clipId); m_binClip = std::static_pointer_cast(item->parent()); m_frameNumber = item->zone().x(); } else { m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_frameNumber < 0) { m_frameNumber = qMax(0, m_binClip->getProducerIntProperty(QStringLiteral("kdenlive:thumbnailFrame"))); } } if (m_binClip->clipType() == ClipType::Audio) { // Don't create thumbnail for audio clips m_done = false; return true; } m_inCache = false; if (ThumbnailCache::get()->hasThumbnail(m_binClip->clipId(), m_frameNumber, !m_persistent)) { m_done = true; m_result = ThumbnailCache::get()->getThumbnail(m_binClip->clipId(), m_frameNumber); m_inCache = true; return true; } m_prod = m_binClip->thumbProducer(); if ((m_prod == nullptr) || !m_prod->is_valid()) { return false; } int max = m_prod->get_length(); m_frameNumber = m_binClip->clipType() == ClipType::Image ? 0 : qMin(m_frameNumber, max - 1); if (m_frameNumber > 0) { m_prod->seek(m_frameNumber); } QScopedPointer frame(m_prod->get_frame()); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); frame->set("rescale.interp", "nearest"); if ((frame != nullptr) && frame->is_valid()) { m_result = KThumb::getFrame(frame.data(), m_fullWidth, m_imageHeight, true); m_done = true; } return m_done; } -bool ThumbJob::commitResult(Fun &undo, Fun &redo) +bool ThumbJob::commitResult(Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(!m_resultConsumed); if (!m_done) { if (m_binClip->clipType() == ClipType::Audio) { // audio files get standard audio icon, ok return true; } qDebug() << "ERROR: Trying to consume invalid results"; return false; } if (!m_inCache) { if (m_result.isNull()) { qDebug() << "+++++\nINVALID RESULT IMAGE\n++++++++++++++"; } else { ThumbnailCache::get()->storeThumbnail(m_binClip->clipId(), m_frameNumber, m_result, m_persistent); } } m_resultConsumed = true; // TODO a refactor of ProjectClip and ProjectSubClip should make that possible without branching (both classes implement setThumbnail) bool ok = false; if (m_subClip) { auto subClip = std::static_pointer_cast(pCore->projectItemModel()->getItemByBinId(m_clipId)); QImage old = subClip->thumbnail(m_result.width(), m_result.height()).toImage(); // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [ clip = subClip, image = std::move(m_result) ]() { clip->setThumbnail(image); return true; }; auto reverse = [ clip = subClip, image = std::move(old) ]() { clip->setThumbnail(image); return true; }; ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } } else { QImage old = m_binClip->thumbnail(m_result.width(), m_result.height()).toImage(); // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [ clip = m_binClip, image = std::move(m_result), this ]() { clip->setThumbnail(image); if (m_reloadAll) { clip->updateTimelineClips({TimelineModel::ReloadThumbRole}); } return true; }; auto reverse = [ clip = m_binClip, image = std::move(old), this ]() { clip->setThumbnail(image); if (m_reloadAll) { clip->updateTimelineClips({TimelineModel::ReloadThumbRole}); } return true; }; ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } } return ok; } diff --git a/src/jobs/thumbjob.hpp b/src/jobs/thumbjob.hpp index 8926e3865..a7fce0684 100644 --- a/src/jobs/thumbjob.hpp +++ b/src/jobs/thumbjob.hpp @@ -1,70 +1,70 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #pragma once #include "abstractclipjob.h" #include #include /* @brief This class represents the job that corresponds to computing the thumb of a clip */ class ProjectClip; namespace Mlt { class Producer; } class ThumbJob : public AbstractClipJob { Q_OBJECT public: /* @brief Extract a thumb for given clip. @param frameNumber is the frame to extract. Leave to -1 for default @param persistent: if true, we will use the persistent cache (for query and saving) */ ThumbJob(const QString &binId, int imageHeight, int frameNumber = -1, bool persistent = false, bool reloadAllThumbs = false); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ - bool commitResult(Fun &undo, Fun &redo) override; + bool commitResult(Fun &undo, Fun &redo, Updates &list) override; private: int m_frameNumber; int m_fullWidth; int m_imageHeight; std::shared_ptr m_binClip; std::shared_ptr m_prod; QImage m_result; bool m_done{false}; bool m_persistent; bool m_reloadAll; bool m_inCache{false}; bool m_subClip{false}; // true if we operate on a subclip }; diff --git a/src/timeline2/CMakeLists.txt b/src/timeline2/CMakeLists.txt index 61c0e3dc5..73f91bf8f 100644 --- a/src/timeline2/CMakeLists.txt +++ b/src/timeline2/CMakeLists.txt @@ -1,21 +1,22 @@ set(kdenlive_SRCS ${kdenlive_SRCS} timeline2/model/clipmodel.cpp timeline2/model/compositionmodel.cpp timeline2/model/groupsmodel.cpp + timeline2/model/modelupdater.cpp timeline2/model/timelineitemmodel.cpp timeline2/model/timelinemodel.cpp timeline2/model/timelinefunctions.cpp timeline2/model/trackmodel.cpp timeline2/model/snapmodel.cpp timeline2/model/builders/meltBuilder.cpp timeline2/view/dialogs/spacerdialog.cpp timeline2/view/dialogs/trackdialog.cpp timeline2/view/dialogs/clipdurationdialog.cpp timeline2/view/previewmanager.cpp timeline2/view/timelinetabs.cpp timeline2/view/timelinecontroller.cpp timeline2/view/timelinewidget.cpp timeline2/view/qml/timelineitems.cpp timeline2/view/qmltypes/thumbnailprovider.cpp PARENT_SCOPE) diff --git a/src/timeline2/model/builders/meltBuilder.cpp b/src/timeline2/model/builders/meltBuilder.cpp index 6962aecbc..71ac128e2 100644 --- a/src/timeline2/model/builders/meltBuilder.cpp +++ b/src/timeline2/model/builders/meltBuilder.cpp @@ -1,289 +1,292 @@ /*************************************************************************** * 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 "meltBuilder.hpp" #include "../clipmodel.hpp" +#include "../modelupdater.hpp" #include "../timelineitemmodel.hpp" #include "../timelinemodel.hpp" #include "../trackmodel.hpp" #include "../undohelper.hpp" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "kdenlivesettings.h" #include #include #include #include #include bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, - const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo); + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, Updates &list); bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, - const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo); + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, Updates &list); bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; // First, we destruct the previous tracks - timeline->requestReset(undo, redo); + timeline->requestReset(undo, redo, list); std::unordered_map binIdCorresp; pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp); QSet reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"), QLatin1String("black_track")}; bool ok = true; qDebug() << "//////////////////////\nTrying to construct" << tractor.count() << "tracks.\n////////////////////////////////"; for (int i = 0; i < tractor.count() && ok; i++) { std::unique_ptr track(tractor.track(i)); QString playlist_name = track->get("id"); if (reserved_names.contains(playlist_name)) { continue; } switch (track->type()) { case producer_type: // TODO check that it is the black track, and otherwise log an error qDebug() << "SUSPICIOUS: we weren't expecting a producer when parsing the timeline"; break; case tractor_type: { // that is a double track int tid; bool audioTrack = track->get_int("kdenlive:audio_track") == 1; - ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false); + ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, list, false); int lockState = track->get_int("kdenlive:locked_track"); Mlt::Tractor local_tractor(*track); - ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo); + ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo, list); timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format")); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } break; } case playlist_type: { // that is a single track qDebug() << "Adding track: " << track->get("id"); int tid; Mlt::Playlist local_playlist(*track); const QString trackName = local_playlist.get("kdenlive:track_name"); bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1; - ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false); + ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, list, false); int muteState = track->get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } int lockState = local_playlist.get_int("kdenlive:locked_track"); - ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo); + ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo, list); timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format")); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } break; } default: qDebug() << "ERROR: Unexpected item in the timeline"; } } // Loading compositions QScopedPointer service(tractor.producer()); QList compositions; while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString id(t.get("kdenlive_id")); QString internal(t.get("internal_added")); if (internal.isEmpty()) { compositions << new Mlt::Transition(t); if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t.get("id") << " = " << t.get("mlt_service"); t.set("kdenlive_id", t.get("mlt_service")); } } } service.reset(service->producer()); } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); Mlt::Properties transProps(t->get_properties()); QString id(t->get("kdenlive_id")); int compoId; ok = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), - t->get_length(), &transProps, compoId, undo, redo); + t->get_length(), &transProps, compoId, undo, redo, list); if (!ok) { qDebug() << "ERROR : failed to insert composition in track " << t->get_b_track() << ", position" << t->get_in(); break; } qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in() << "/" << t->get_out(); } } // build internal track compositing timeline->buildTrackCompositing(); timeline->updateDuration(); if (!ok) { // TODO log error undo(); return false; } + ModelUpdater::applyUpdates(undo, redo, list); return true; } bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, - const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo) + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, Updates &list) { if (track.count() != 2) { // we expect a tractor with two tracks (a "fake" track) qDebug() << "ERROR : wrong number of subtracks"; return false; } for (int i = 0; i < track.count(); i++) { std::unique_ptr sub_track(track.track(i)); if (sub_track->type() != playlist_type) { qDebug() << "ERROR : SubTracks must be MLT::Playlist"; return false; } Mlt::Playlist playlist(*sub_track); - constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo); + constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo, list); if (i == 0) { // Pass track properties int height = track.get_int("kdenlive:trackheight"); timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height)); timeline->setTrackProperty(tid, "kdenlive:collapsed", QString::number(track.get_int("kdenlive:collapsed"))); QString trackName = track.get("kdenlive:track_name"); if (!trackName.isEmpty()) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData()); } bool audioTrack = track.get_int("kdenlive:audio_track") == 1; if (audioTrack) { // This is an audio track timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("1")); } else { // video track, hide audio timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("2")); } int muteState = playlist.get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } } } std::shared_ptr serv = std::make_shared(track.get_service()); timeline->importTrackEffects(tid, serv); return true; } namespace { // This function tries to recover the state of the producer (audio or video or both) PlaylistState::ClipState inferState(std::shared_ptr prod, bool audioTrack) { auto getProperty = [prod](const QString &name) { if (prod->parent().is_valid()) { return QString::fromUtf8(prod->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(prod->get(name.toUtf8().constData())); }; auto getIntProperty = [prod](const QString &name) { if (prod->parent().is_valid()) { return prod->parent().get_int(name.toUtf8().constData()); } return prod->get_int(name.toUtf8().constData()); }; QString service = getProperty("mlt_service"); std::pair VidAud{true, true}; VidAud.first = getIntProperty("set.test_image") == 0; VidAud.second = getIntProperty("set.test_audio") == 0; if (audioTrack || (service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1)) { VidAud.first = false; } if (!audioTrack || (service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1)) { VidAud.second = false; } return stateFromBool(VidAud); } } // namespace bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, - const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo) + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, Updates &list) { bool audioTrack = track.get_int("kdenlive:audio_track") == 1; for (int i = 0; i < track.count(); i++) { if (track.is_blank(i)) { continue; } std::shared_ptr clip(track.get_clip(i)); int position = track.clip_start(i); switch (clip->type()) { case unknown_type: case producer_type: { // qDebug() << "Looking for clip clip "<< clip->parent().get("kdenlive:id")<<" = "<parent().get("kdenlive:clipname"); QString binId; if (clip->parent().get_int("_kdenlive_processed") == 1) { // This is a bin clip, already processed no need to change id binId = QString(clip->parent().get("kdenlive:id")); } else { QString clipId = clip->parent().get("kdenlive:id"); if (clipId.startsWith(QStringLiteral("slowmotion"))) { clipId = clipId.section(QLatin1Char(':'), 1, 1); } if (clipId.isEmpty()) { clipId = clip->get("kdenlive:id"); } Q_ASSERT(binIdCorresp.count(clipId) > 0); binId = binIdCorresp.at(clipId); } bool ok = false; if (pCore->bin()->getBinClip(binId)) { PlaylistState::ClipState st = inferState(clip, audioTrack); int cid = ClipModel::construct(timeline, binId, clip, st); - ok = timeline->requestClipMove(cid, tid, position, true, false, undo, redo); + ok = timeline->requestClipMove(cid, tid, position, true, false, undo, redo, list); } else { qDebug() << "// Cannot find bin clip: " << binId << " - " << clip->get("id"); } if (!ok) { qDebug() << "ERROR : failed to insert clip in track" << tid << "position" << position; return false; } qDebug() << "Inserted clip in track" << tid << "at " << position; break; } case tractor_type: { // TODO This is a nested timeline qDebug() << "NOT_IMPLEMENTED: code for parsing nested timeline is not there yet."; break; } default: qDebug() << "ERROR : unexpected object found on playlist"; return false; break; } } return true; } diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index b7f4edf2c..68a69b9ef 100644 --- a/src/timeline2/model/clipmodel.cpp +++ b/src/timeline2/model/clipmodel.cpp @@ -1,570 +1,565 @@ /*************************************************************************** * 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 "clipmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "macros.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include #include #include // this can be deleted #include "bin/model/markerlistmodel.hpp" #include "gentime.h" #include ClipModel::ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, double speed) : MoveableItem(parent, id) , m_producer(std::move(prod)) , m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack)) , m_binClipId(binClipId) , forceThumbReload(false) , m_currentState(state) , m_speed(speed) { m_producer->set("kdenlive:id", binClipId.toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); m_canBeVideo = binClip->hasVideo(); m_canBeAudio = binClip->hasAudio(); m_clipType = binClip->clipType(); if (binClip) { m_endlessResize = !binClip->hasLimitedDuration(); } else { m_endlessResize = false; } } int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state) { id = (id == -1 ? TimelineModel::getNextId() : id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); std::shared_ptr cutProducer = binClip->getTimelineProducer(id, state, 1.); std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id, state)); clip->setClipState_lambda(state)(); parent->registerClip(clip); return id; } int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, PlaylistState::ClipState state) { // we hand the producer to the bin clip, and in return we get a cut to a good master producer // We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other // clipModel (due to a mlt limitation, see ProjectClip doc) int id = TimelineModel::getNextId(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); double speed = 1.0; if (QString::fromUtf8(producer->get("mlt_service")) == QLatin1String("timewarp")) { speed = producer->get_double("warp_speed"); } auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state); std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id, state, speed)); clip->m_effectStack->importEffects(producer, result.second); clip->setClipState_lambda(state)(); parent->registerClip(clip); return id; } void ClipModel::registerClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (!binClip) { qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!"; } qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count(); binClip->registerTimelineClip(m_parent, m_id); } void ClipModel::deregisterClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); binClip->deregisterTimelineClip(m_id); } ClipModel::~ClipModel() {} -bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo) +bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, Updates &list, bool logUndo) { QWriteLocker locker(&m_lock); // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" << // m_producer->get_length(); if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) { return false; } int delta = getPlaytime() - size; if (delta == 0) { return true; } int in = m_producer->get_in(); int out = m_producer->get_out(); int old_in = in, old_out = out; // check if there is enough space on the chosen side if (!right && in + delta < 0 && !m_endlessResize) { return false; } if (!m_endlessResize && right && out - delta >= m_producer->get_length()) { return false; } if (right) { out -= delta; } else { in += delta; } // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out; std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; int outPoint = out; int inPoint = in; if (m_endlessResize) { outPoint = out - in; inPoint = 0; } if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right); } else { qDebug() << "Error : Moving clip failed because parent timeline is not available anymore"; Q_ASSERT(false); } } else { // Ensure producer is long enough if (m_endlessResize && outPoint > m_producer->parent().get_length()) { m_producer->set("length", outPoint + 1); } } Fun operation = [this, inPoint, outPoint, track_operation]() { if (track_operation()) { m_producer->set_in_and_out(inPoint, outPoint); return true; } return false; }; if (operation()) { - // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here - auto ptr = m_parent.lock(); - if (m_currentTrackId != -1 && ptr) { - if (!right) { - QModelIndex ix = ptr->makeClipIndexFromID(m_id); - ptr->dataChanged(ix, ix, {TimelineModel::InPointRole}); - } - track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right); + // we send a list of roles to be updated + QVector roles{TimelineModel::DurationRole}; + if (right) { + roles.push_back(TimelineModel::StartRole); + roles.push_back(TimelineModel::InPointRole); + } else { + roles.push_back(TimelineModel::OutPointRole); } + list.emplace_back(std::make_shared(getId(), m_parent, roles)); + // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here Fun reverse = [this, old_in, old_out, track_reverse]() { if (track_reverse()) { m_producer->set_in_and_out(old_in, old_out); return true; } return false; }; qDebug() << "// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", " << m_producer->get_playtime(); if (logUndo) { // adjustEffectLength(right, old_in, inPoint, oldDuration, m_producer->get_playtime(), reverse, operation, logUndo); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString ClipModel::getProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return QString::fromUtf8(service()->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(service()->get(name.toUtf8().constData())); } int ClipModel::getIntProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_int(name.toUtf8().constData()); } return service()->get_int(name.toUtf8().constData()); } QSize ClipModel::getFrameSize() const { READ_LOCK(); if (service()->parent().is_valid()) { return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height")); } return QSize(service()->get_int("meta.media.width"), service()->get_int("meta.media.height")); } double ClipModel::getDoubleProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_double(name.toUtf8().constData()); } return service()->get_double(name.toUtf8().constData()); } Mlt::Producer *ClipModel::service() const { READ_LOCK(); return m_producer.get(); } int ClipModel::getPlaytime() const { READ_LOCK(); return m_producer->get_playtime(); } void ClipModel::setTimelineEffectsEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStack->setEffectStackEnabled(enabled); } bool ClipModel::addEffect(const QString &effectId) { QWriteLocker locker(&m_lock); if (EffectsRepository::get()->getType(effectId) == EffectType::Audio) { if (m_currentState == PlaylistState::VideoOnly) { return false; } } else if (m_currentState == PlaylistState::AudioOnly) { return false; } m_effectStack->appendEffect(effectId); return true; } bool ClipModel::copyEffect(std::shared_ptr stackModel, int rowId) { QWriteLocker locker(&m_lock); m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId)); return true; } bool ClipModel::importEffects(std::shared_ptr stackModel) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(stackModel); return true; } bool ClipModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(service); return true; } bool ClipModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); m_effectStack->removeFade(fromStart); return true; } -bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, bool logUndo) +bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, Updates &list, bool logUndo) { QWriteLocker locker(&m_lock); return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, undo, redo, logUndo); } -bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo) +bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName; Fun operation = [this, duration, effectName]() { return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly()); }; if (operation() && originalDuration > 0) { Fun reverse = [this, originalDuration, effectName]() { return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly()); }; UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return true; } bool ClipModel::audioEnabled() const { READ_LOCK(); return stateToBool(m_currentState).second; } bool ClipModel::isAudioOnly() const { READ_LOCK(); return m_currentState == PlaylistState::AudioOnly; } void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state, double speed) { // We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip // first, refresh, and then replant. Q_ASSERT(m_currentTrackId == -1); QWriteLocker locker(&m_lock); int in = getIn(); int out = getOut(); qDebug() << "refresh " << speed << m_speed << in << out; if (!qFuzzyCompare(speed, m_speed) && !qFuzzyCompare(speed, 0.)) { in = in * m_speed / speed; out = in + getPlaytime() - 1; // prevent going out of the clip's range out = std::min(out, int(double(m_producer->get_length()) * m_speed / speed) - 1); m_speed = speed; qDebug() << "changing speed" << in << out << m_speed; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); std::shared_ptr binProducer = binClip->getTimelineProducer(m_id, state, m_speed); m_producer = std::move(binProducer); m_producer->set_in_and_out(in, out); // replant effect stack in updated service m_effectStack->resetService(m_producer); m_producer->set("kdenlive:id", binClip->AbstractProjectItem::clipId().toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); m_endlessResize = !binClip->hasLimitedDuration(); } void ClipModel::refreshProducerFromBin() { refreshProducerFromBin(m_currentState); } -bool ClipModel::useTimewarpProducer(double speed, Fun &undo, Fun &redo) +bool ClipModel::useTimewarpProducer(double speed, Fun &undo, Fun &redo, Updates &list) { if (m_endlessResize) { // no timewarp for endless producers return false; } if (qFuzzyCompare(speed, m_speed)) { // nothing to do return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; double previousSpeed = getSpeed(); int oldDuration = getPlaytime(); int newDuration = int(double(oldDuration) * previousSpeed / speed); int oldOut = getOut(); int oldIn = getIn(); auto operation = useTimewarpProducer_lambda(speed); auto reverse = useTimewarpProducer_lambda(previousSpeed); if (oldOut >= newDuration) { // in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer reverse = [reverse, oldIn, oldOut, this]() { bool res = reverse(); if (res) { setInOut(oldIn, oldOut); } return res; }; } if (operation()) { + list.emplace_back(new ChangeUpdate(getId(), m_parent, {TimelineModel::SpeedRole})); UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); - bool res = requestResize(newDuration, true, local_undo, local_redo, true); + bool res = requestResize(newDuration, true, local_undo, local_redo, list, true); if (!res) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } qDebug() << "tw: operation fail"; return false; } Fun ClipModel::useTimewarpProducer_lambda(double speed) { QWriteLocker locker(&m_lock); return [speed, this]() { qDebug() << "timeWarp producer" << speed; refreshProducerFromBin(m_currentState, speed); - if (auto ptr = m_parent.lock()) { - QModelIndex ix = ptr->makeClipIndexFromID(m_id); - ptr->notifyChange(ix, ix, TimelineModel::SpeedRole); - } return true; }; } QVariant ClipModel::getAudioWaveform() { READ_LOCK(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (binClip) { return QVariant::fromValue(binClip->audioFrameCache); } return QVariant(); } const QString &ClipModel::binId() const { return m_binClipId; } std::shared_ptr ClipModel::getMarkerModel() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel(); } int ClipModel::fadeIn() const { return m_effectStack->getFadePosition(true); } int ClipModel::fadeOut() const { return m_effectStack->getFadePosition(false); } double ClipModel::getSpeed() const { return m_speed; } KeyframeModel *ClipModel::getKeyframeModel() { return m_effectStack->getEffectKeyframeModel(); } bool ClipModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void ClipModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); return [this, state]() { if (auto ptr = m_parent.lock()) { switch (state) { case PlaylistState::Disabled: m_producer->set("set.test_audio", 1); m_producer->set("set.test_image", 1); break; case PlaylistState::VideoOnly: m_producer->set("set.test_image", 0); break; case PlaylistState::AudioOnly: m_producer->set("set.test_audio", 0); break; default: // error break; } m_currentState = state; - if (ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case - QModelIndex ix = ptr->makeClipIndexFromID(m_id); - ptr->dataChanged(ix, ix, {TimelineModel::StatusRole}); - } return true; } return false; }; } -bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo, Updates &list) { if (state == PlaylistState::VideoOnly && !canBeVideo()) { return false; } if (state == PlaylistState::AudioOnly && !canBeAudio()) { return false; } if (state == m_currentState) { return true; } auto old_state = m_currentState; auto operation = setClipState_lambda(state); if (operation()) { + list.emplace_back(new ChangeUpdate(getId(), m_parent, {TimelineModel::StatusRole})); auto reverse = setClipState_lambda(old_state); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } PlaylistState::ClipState ClipModel::clipState() const { READ_LOCK(); return m_currentState; } ClipType::ProducerType ClipModel::clipType() const { READ_LOCK(); return m_clipType; } void ClipModel::passTimelineProperties(std::shared_ptr other) { READ_LOCK(); Mlt::Properties source(m_producer->get_properties()); Mlt::Properties dest(other->service()->get_properties()); dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect"); } bool ClipModel::canBeVideo() const { return m_canBeVideo; } bool ClipModel::canBeAudio() const { return m_canBeAudio; } const QString ClipModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } diff --git a/src/timeline2/model/clipmodel.hpp b/src/timeline2/model/clipmodel.hpp index 5da758799..6bdbc3996 100644 --- a/src/timeline2/model/clipmodel.hpp +++ b/src/timeline2/model/clipmodel.hpp @@ -1,195 +1,195 @@ /*************************************************************************** * 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 CLIPMODEL_H #define CLIPMODEL_H #include "moveableItem.hpp" #include "undohelper.hpp" #include #include namespace Mlt { class Producer; } class EffectStackModel; class MarkerListModel; class ProjectClip; class TimelineModel; class TrackModel; class KeyframeModel; /* @brief This class represents a Clip object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class ClipModel : public MoveableItem { ClipModel() = delete; protected: /* This constructor is not meant to be called, call the static construct instead */ ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, double speed = 1.); public: ~ClipModel(); /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip @param parent is a pointer to the timeline @param binClip is the id of the bin clip associated @param id Requested id of the clip. Automatic if -1 */ static int construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state); /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip This variants assumes a producer is already known, which should typically happen only at loading time. Note that there is no guarantee that this producer is actually going to be used. It might be discarded. */ static int construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, PlaylistState::ClipState state); /* @brief returns a property of the clip, or from it's parent if it's a cut */ const QString getProperty(const QString &name) const override; int getIntProperty(const QString &name) const; double getDoubleProperty(const QString &name) const; QSize getFrameSize() const; Q_INVOKABLE bool showKeyframes() const; Q_INVOKABLE void setShowKeyframes(bool show); /* @brief Returns true if the clip can be converted to a video clip */ bool canBeVideo() const; /* @brief Returns true if the clip can be converted to an audio clip */ bool canBeAudio() const; /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; /** @brief Returns the timeline clip status (video / audio only) */ PlaylistState::ClipState clipState() const; /** @brief Returns the bin clip type (image, color, AV, ...) */ ClipType::ProducerType clipType() const; /** @brief Sets the timeline clip status (video / audio only) */ - bool setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo); + bool setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo, Updates &list); protected: // helper fuctions that creates the lambda Fun setClipState_lambda(PlaylistState::ClipState state); public: /* @brief returns the length of the item on the timeline */ int getPlaytime() const override; /** @brief Returns audio cache data from bin clip to display audio thumbs */ QVariant getAudioWaveform(); /** @brief Returns the bin clip's id */ const QString &binId() const; void registerClipToBin(); void deregisterClipToBin(); bool addEffect(const QString &effectId); bool copyEffect(std::shared_ptr stackModel, int rowId); /* @brief Import effects from a different stackModel */ bool importEffects(std::shared_ptr stackModel); /* @brief Import effects from a service that contains some (another clip?) */ bool importEffects(std::weak_ptr service); bool removeFade(bool fromStart); /** @brief Adjust effects duration. Should be called after each resize / cut operation */ - bool adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, bool logUndo); - bool adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo); + bool adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, Updates &list, bool logUndo); + bool adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo, Updates &list); void passTimelineProperties(std::shared_ptr other); KeyframeModel *getKeyframeModel(); int fadeIn() const; int fadeOut() const; friend class TrackModel; friend class TimelineModel; friend class TimelineItemModel; friend class TimelineController; friend struct TimelineFunctions; protected: Mlt::Producer *service() const override; /* @brief Performs a resize of the given clip. Returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. If a snap point is within reach, the operation will be coerced to use it. @param size is the new size of the clip @param right is true if we change the right side of the clip, false otherwise @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) override; + bool requestResize(int size, bool right, Fun &undo, Fun &redo, Updates &list, bool logUndo = true) override; /* @brief This function change the global (timeline-wise) enabled state of the effects */ void setTimelineEffectsEnabled(bool enabled); /* @brief This functions should be called when the producer of the binClip changes, to allow refresh * @param state corresponds to the state of the clip we want (audio or video) * @param speed corresponds to the speed we need. Leave to 0 to keep current speed. Warning: this function doesn't notify the model. Unless you know what * you are doing, better use useTimewarProducer to change the speed */ void refreshProducerFromBin(PlaylistState::ClipState state, double speed = 0); void refreshProducerFromBin(); /* @brief This functions replaces the current producer with a slowmotion one It also resizes the producer so that set of frames contained in the clip is the same */ - bool useTimewarpProducer(double speed, Fun &undo, Fun &redo); + bool useTimewarpProducer(double speed, Fun &undo, Fun &redo, Updates &list); // @brief Lambda that merely changes the speed (in and out are untouched) Fun useTimewarpProducer_lambda(double speed); /** @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; bool audioEnabled() const; bool isAudioOnly() const; double getSpeed() const; protected: std::shared_ptr m_producer; std::shared_ptr m_effectStack; QString m_binClipId; // This is the Id of the bin clip this clip corresponds to. bool m_endlessResize; // Whether this clip can be freely resized bool forceThumbReload; // Used to trigger a forced thumb reload, when producer changes PlaylistState::ClipState m_currentState; ClipType::ProducerType m_clipType; double m_speed = -1; // Speed of the clip bool m_canBeVideo, m_canBeAudio; }; #endif diff --git a/src/timeline2/model/compositionmodel.cpp b/src/timeline2/model/compositionmodel.cpp index ea57fce3e..bf1614700 100644 --- a/src/timeline2/model/compositionmodel.cpp +++ b/src/timeline2/model/compositionmodel.cpp @@ -1,221 +1,230 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * 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 "compositionmodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "undohelper.hpp" #include #include #include CompositionModel::CompositionModel(std::weak_ptr parent, Mlt::Transition *transition, int id, const QDomElement &transitionXml, const QString &transitionId) : MoveableItem(std::move(parent), id) , AssetParameterModel(transition, transitionXml, transitionId, {ObjectType::TimelineComposition, m_id}) , a_track(-1) { m_compositionName = TransitionsRepository::get()->getName(transitionId); } int CompositionModel::construct(const std::weak_ptr &parent, const QString &transitionId, int id, Mlt::Properties *sourceProperties) { Mlt::Transition *transition = TransitionsRepository::get()->getTransition(transitionId); transition->set_in_and_out(0, 0); auto xml = TransitionsRepository::get()->getXml(transitionId); if (sourceProperties) { // Paste parameters from existing source composition QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); ++i) { QDomElement currentParameter = params.item(i).toElement(); QString paramName = currentParameter.attribute(QStringLiteral("name")); QString paramValue = sourceProperties->get(paramName.toUtf8().constData()); if (!paramValue.isEmpty()) { currentParameter.setAttribute(QStringLiteral("value"), paramValue); } } } std::shared_ptr composition(new CompositionModel(parent, transition, id, xml, transitionId)); id = composition->m_id; if (auto ptr = parent.lock()) { ptr->registerComposition(composition); } else { qDebug() << "Error : construction of composition failed because parent timeline is not available anymore"; Q_ASSERT(false); } return id; } -bool CompositionModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool /*logUndo*/) +bool CompositionModel::requestResize(int size, bool right, Fun &undo, Fun &redo, Updates &list, bool /*logUndo*/) { QWriteLocker locker(&m_lock); if (size <= 0) { return false; } int delta = getPlaytime() - size; qDebug() << "compo request resize " << size << right << delta; int in = getIn(); int out = getOut(); int oldDuration = out - in; int old_in = in, old_out = out; if (right) { out -= delta; } else { in += delta; } // if the in becomes negative, we add the necessary length in out. if (in < 0) { out = out - in; in = 0; } std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, in, out); } else { qDebug() << "Error : Moving composition failed because parent timeline is not available anymore"; Q_ASSERT(false); } } Fun operation = [in, out, track_operation, this]() { if (track_operation()) { return true; } return false; }; if (operation()) { + // we send a list of roles to be updated + QVector roles{TimelineModel::DurationRole}; + if (right) { + roles.push_back(TimelineModel::StartRole); + roles.push_back(TimelineModel::InPointRole); + } else { + roles.push_back(TimelineModel::OutPointRole); + } + list.emplace_back(std::make_shared(getId(), m_parent, roles)); // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here auto ptr = m_parent.lock(); if (m_currentTrackId != -1 && ptr) { track_reverse = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, old_in, old_out); } Fun reverse = [old_in, old_out, track_reverse, this]() { if (track_reverse()) { return true; } return false; }; auto kfr = getKeyframeModel(); if (kfr) { // Adjust keyframe length kfr->resizeKeyframes(0, oldDuration, 0, out, undo, redo); Fun refresh = [kfr, this]() { kfr->modelChanged(); return true; }; refresh(); UPDATE_UNDO_REDO(refresh, refresh, undo, redo); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString CompositionModel::getProperty(const QString &name) const { READ_LOCK(); return QString::fromUtf8(service()->get(name.toUtf8().constData())); } Mlt::Transition *CompositionModel::service() const { READ_LOCK(); return static_cast(m_asset.get()); } Mlt::Properties *CompositionModel::properties() { READ_LOCK(); return new Mlt::Properties(m_asset.get()->get_properties()); } int CompositionModel::getPlaytime() const { READ_LOCK(); return getOut() - getIn() + 1; } int CompositionModel::getATrack() const { READ_LOCK(); return a_track == -1 ? -1 : service()->get_int("a_track"); } void CompositionModel::setForceTrack(bool force) { READ_LOCK(); service()->set("force_track", force ? 1 : 0); } int CompositionModel::getForcedTrack() const { QWriteLocker locker(&m_lock); return (service()->get_int("force_track") == 0 || a_track == -1) ? -1 : service()->get_int("a_track"); } void CompositionModel::setATrack(int trackMltPosition, int trackId) { QWriteLocker locker(&m_lock); Q_ASSERT(trackId != getCurrentTrackId()); // can't compose with same track a_track = trackMltPosition; if (a_track >= 0) { service()->set("a_track", trackMltPosition); } emit compositionTrackChanged(); } KeyframeModel *CompositionModel::getEffectKeyframeModel() { if (getKeyframeModel()) { return getKeyframeModel()->getKeyModel(); } return nullptr; } bool CompositionModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void CompositionModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } const QString &CompositionModel::displayName() const { return m_compositionName; } void CompositionModel::setInOut(int in, int out) { m_position = in; MoveableItem::setInOut(in, out); } diff --git a/src/timeline2/model/compositionmodel.hpp b/src/timeline2/model/compositionmodel.hpp index cd054c4e2..38133f4ed 100644 --- a/src/timeline2/model/compositionmodel.hpp +++ b/src/timeline2/model/compositionmodel.hpp @@ -1,114 +1,114 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * 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 COMPOSITIONMODEL_H #define COMPOSITIONMODEL_H #include "assets/model/assetparametermodel.hpp" #include "moveableItem.hpp" #include "undohelper.hpp" #include #include namespace Mlt { class Transition; } class TimelineModel; class TrackModel; class KeyframeModel; /* @brief This class represents a Composition object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class CompositionModel : public MoveableItem, public AssetParameterModel { CompositionModel() = delete; protected: /* This constructor is not meant to be called, call the static construct instead */ CompositionModel(std::weak_ptr parent, Mlt::Transition *transition, int id, const QDomElement &transitionXml, const QString &transitionId); public: /* @brief Creates a composition, which then registers itself to the parent timeline Returns the (unique) id of the created composition @param parent is a pointer to the timeline @param transitionId is the id of the transition to be inserted @param id Requested id of the clip. Automatic if -1 */ static int construct(const std::weak_ptr &parent, const QString &transitionId, int id = -1, Mlt::Properties *sourceProperties = nullptr); friend class TrackModel; friend class TimelineModel; /* @brief returns the length of the item on the timeline */ int getPlaytime() const override; /* @brief Returns the id of the second track involved in the composition (a_track in mlt's vocabulary, the b_track beeing the track where the composition is inserted) */ int getATrack() const; /* @brief Defines the forced_track property. If true, the a_track will not change when composition * is moved to another track. When false, the a_track will automatically change to lower video track */ void setForceTrack(bool force); /* @brief Returns the id of the second track involved in the composition (a_track) or -1 if the a_track should be automatically updated when the composition * changes track */ int getForcedTrack() const; /* @brief Sets the id of the second track involved in the composition*/ void setATrack(int trackMltPosition, int trackId); /* @brief returns a property of the current item */ const QString getProperty(const QString &name) const override; /* @brief returns the active effect's keyframe model */ KeyframeModel *getEffectKeyframeModel(); Q_INVOKABLE bool showKeyframes() const; Q_INVOKABLE void setShowKeyframes(bool show); const QString &displayName() const; Mlt::Properties *properties(); protected: Mlt::Transition *service() const override; void setInOut(int in, int out) override; /* @brief Performs a resize of the given composition. Returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. If a snap point is within reach, the operation will be coerced to use it. @param size is the new size of the composition @param right is true if we change the right side of the composition, false otherwise @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) override; + bool requestResize(int size, bool right, Fun &undo, Fun &redo, Updates &list, bool logUndo = true) override; private: int a_track; QString m_compositionName; }; #endif diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp index 1aed63ad3..5e588d698 100644 --- a/src/timeline2/model/groupsmodel.cpp +++ b/src/timeline2/model/groupsmodel.cpp @@ -1,941 +1,942 @@ /*************************************************************************** * 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 "groupsmodel.hpp" #include "macros.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include GroupsModel::GroupsModel(std::weak_ptr parent) : m_parent(std::move(parent)) , m_lock(QReadWriteLock::Recursive) { } void GroupsModel::promoteToGroup(int gid, GroupType type) { Q_ASSERT(type != GroupType::Leaf); Q_ASSERT(m_groupIds.count(gid) == 0); m_groupIds.insert({gid, type}); auto ptr = m_parent.lock(); if (ptr) { // qDebug() << "Registering group" << gid << "of type" << groupTypeToStr(getType(gid)); ptr->registerGroup(gid); } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } void GroupsModel::downgradeToLeaf(int gid) { Q_ASSERT(m_groupIds.count(gid) != 0); Q_ASSERT(m_downLink.at(gid).size() == 0); auto ptr = m_parent.lock(); if (ptr) { // qDebug() << "Deregistering group" << gid << "of type" << groupTypeToStr(getType(gid)); ptr->deregisterGroup(gid); m_groupIds.erase(gid); } else { qDebug() << "Impossible to ungroup item because the timeline is not available anymore"; Q_ASSERT(false); } } Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set &ids, GroupType type, int parent) { QWriteLocker locker(&m_lock); Q_ASSERT(ids.size() == 0 || type != GroupType::Leaf); return [gid, ids, parent, type, this]() { createGroupItem(gid); if (parent != -1) { setGroup(gid, parent); } if (ids.size() > 0) { promoteToGroup(gid, type); std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); }); - auto ptr = m_parent.lock(); - if (!ptr) Q_ASSERT(false); for (int id : roots) { - setGroup(getRootId(id), gid, type != GroupType::Selection); + setGroup(getRootId(id), gid); } } return true; }; } -int GroupsModel::groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type, bool force) +int GroupsModel::groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, Updates &list, GroupType type, bool force) { QWriteLocker locker(&m_lock); Q_ASSERT(type != GroupType::Leaf); Q_ASSERT(!ids.empty()); if (ids.size() == 1 && !force) { // We do not create a group with only one element. Instead, we return the id of that element return *(ids.begin()); } int gid = TimelineModel::getNextId(); auto operation = groupItems_lambda(gid, ids, type); if (operation()) { + if (type != GroupType::Selection) { + if (auto ptr = m_parent.lock()) { + for (int id : ids) { + if (ptr->isClip(id) || ptr->isComposition(id)) { + list.emplace_back(new ChangeUpdate(id, m_parent, {TimelineModel::GroupedRole})); + } + } + } + } auto reverse = destructGroupItem_lambda(gid); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return gid; } return -1; } -bool GroupsModel::ungroupItem(int id, Fun &undo, Fun &redo) +bool GroupsModel::ungroupItem(int id, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); int gid = getRootId(id); if (m_groupIds.count(gid) == 0) { // element is not part of a group return false; } - - return destructGroupItem(gid, true, undo, redo); + return destructGroupItem(gid, true, undo, redo, list); } void GroupsModel::createGroupItem(int id) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) == 0); Q_ASSERT(m_downLink.count(id) == 0); m_upLink[id] = -1; m_downLink[id] = std::unordered_set(); } Fun GroupsModel::destructGroupItem_lambda(int id) { QWriteLocker locker(&m_lock); return [this, id]() { removeFromGroup(id); - auto ptr = m_parent.lock(); - if (!ptr) Q_ASSERT(false); for (int child : m_downLink[id]) { m_upLink[child] = -1; - if (ptr->isClip(child)) { - QModelIndex ix = ptr->makeClipIndexFromID(child); - ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); - } } m_downLink[id].clear(); if (getType(id) != GroupType::Leaf) { downgradeToLeaf(id); } m_downLink.erase(id); m_upLink.erase(id); return true; }; } -bool GroupsModel::destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo) +bool GroupsModel::destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); int parent = m_upLink[id]; auto old_children = m_downLink[id]; auto old_type = getType(id); auto old_parent_type = GroupType::Normal; + auto old_leaves = getLeaves(id); if (parent != -1) { old_parent_type = getType(parent); } auto operation = destructGroupItem_lambda(id); if (operation()) { + if (auto ptr = m_parent.lock()) { + for (int curId : old_leaves) { + if (ptr->isClip(id) || ptr->isComposition(id)) { + list.emplace_back(new ChangeUpdate(curId, m_parent, {TimelineModel::GroupedRole})); + } + } + } + auto reverse = groupItems_lambda(id, old_children, old_type, parent); // we may need to reset the group of the parent if (parent != -1) { auto setParent = [&, old_parent_type, parent]() { setType(parent, old_parent_type); return true; }; PUSH_LAMBDA(setParent, reverse); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); if (parent != -1 && m_downLink[parent].empty() && deleteOrphan) { - return destructGroupItem(parent, true, undo, redo); + return destructGroupItem(parent, true, undo, redo, list); } return true; } return false; } bool GroupsModel::destructGroupItem(int id) { QWriteLocker locker(&m_lock); return destructGroupItem_lambda(id)(); } int GroupsModel::getRootId(int id) const { READ_LOCK(); std::unordered_set seen; // we store visited ids to detect cycles int father = -1; do { Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(seen.count(id) == 0); seen.insert(id); father = m_upLink.at(id); if (father != -1) { id = father; } } while (father != -1); return id; } bool GroupsModel::isLeaf(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return m_downLink.at(id).empty(); } bool GroupsModel::isInGroup(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return getRootId(id) != id; } int GroupsModel::getSplitPartner(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); int groupId = m_upLink.at(id); if (groupId == -1 || getType(groupId) != GroupType::AVSplit) { // clip does not have an AV split partner return -1; } std::unordered_set leaves = getDirectChildren(groupId); if (leaves.size() != 2) { // clip does not have an AV split partner qDebug() << "WRONG SPLIT GROUP SIZE: " << leaves.size(); return -1; } for (const int &child : leaves) { if (child != id) { return child; } } return -1; } std::unordered_set GroupsModel::getSubtree(int id) const { READ_LOCK(); std::unordered_set result; result.insert(id); std::queue queue; queue.push(id); while (!queue.empty()) { int current = queue.front(); queue.pop(); for (const int &child : m_downLink.at(current)) { result.insert(child); queue.push(child); } } return result; } std::unordered_set GroupsModel::getLeaves(int id) const { READ_LOCK(); std::unordered_set result; std::queue queue; queue.push(id); while (!queue.empty()) { int current = queue.front(); queue.pop(); for (const int &child : m_downLink.at(current)) { queue.push(child); } if (m_downLink.at(current).empty()) { result.insert(current); } } return result; } std::unordered_set GroupsModel::getDirectChildren(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return m_downLink.at(id); } int GroupsModel::getDirectAncestor(int id) const { READ_LOCK(); Q_ASSERT(m_upLink.count(id) > 0); return m_upLink.at(id); } -void GroupsModel::setGroup(int id, int groupId, bool changeState) +void GroupsModel::setGroup(int id, int groupId) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(groupId == -1 || m_downLink.count(groupId) > 0); Q_ASSERT(id != groupId); removeFromGroup(id); m_upLink[id] = groupId; if (groupId != -1) { m_downLink[groupId].insert(id); - auto ptr = m_parent.lock(); - if (changeState && ptr) { - QModelIndex ix; - if (ptr->isClip(id)) { - ix = ptr->makeClipIndexFromID(id); - } else if (ptr->isComposition(id)) { - ix = ptr->makeCompositionIndexFromID(id); - } - if (ix.isValid()) { - ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); - } - } if (getType(groupId) == GroupType::Leaf) { promoteToGroup(groupId, GroupType::Normal); } } } void GroupsModel::removeFromGroup(int id) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(m_downLink.count(id) > 0); int parent = m_upLink[id]; if (parent != -1) { Q_ASSERT(getType(parent) != GroupType::Leaf); m_downLink[parent].erase(id); QModelIndex ix; auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); if (ptr->isClip(id)) { ix = ptr->makeClipIndexFromID(id); } else if (ptr->isComposition(id)) { ix = ptr->makeCompositionIndexFromID(id); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } if (m_downLink[parent].size() == 0) { downgradeToLeaf(parent); } } m_upLink[id] = -1; } -bool GroupsModel::mergeSingleGroups(int id, Fun &undo, Fun &redo) +bool GroupsModel::mergeSingleGroups(int id, Fun &undo, Fun &redo, Updates &list) { // The idea is as follow: we start from the leaves, and go up to the root. // In the process, if we find a node with only one children, we flag it for deletion QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); auto leaves = getLeaves(id); std::unordered_map old_parents, new_parents; std::vector to_delete; std::unordered_set processed; // to avoid going twice along the same branch for (int leaf : leaves) { int current = m_upLink[leaf]; int start = leaf; while (current != m_upLink[id] && processed.count(current) == 0) { processed.insert(current); if (m_downLink[current].size() == 1) { to_delete.push_back(current); } else { if (current != m_upLink[start]) { old_parents[start] = m_upLink[start]; new_parents[start] = current; } start = current; } current = m_upLink[current]; } if (current != m_upLink[start]) { old_parents[start] = m_upLink[start]; new_parents[start] = current; } } auto parent_changer = [this](const std::unordered_map &parents) { auto ptr = m_parent.lock(); if (!ptr) { qDebug() << "Impossible to create group because the timeline is not available anymore"; return false; } for (const auto &group : parents) { setGroup(group.first, group.second); } return true; }; Fun reverse = [this, old_parents, parent_changer]() { return parent_changer(old_parents); }; Fun operation = [this, new_parents, parent_changer]() { return parent_changer(new_parents); }; bool res = operation(); if (!res) { bool undone = reverse(); Q_ASSERT(undone); return res; } UPDATE_UNDO_REDO(operation, reverse, undo, redo); for (int gid : to_delete) { Q_ASSERT(m_downLink[gid].size() == 0); if (getType(gid) == GroupType::Selection) { continue; } - res = destructGroupItem(gid, false, undo, redo); + res = destructGroupItem(gid, false, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); return res; } } return true; } -bool GroupsModel::split(int id, const std::function &criterion, Fun &undo, Fun &redo) +bool GroupsModel::split(int id, const std::function &criterion, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (isLeaf(id)) { return true; } // This function is valid only for roots (otherwise it is not clear what should be the new parent of the created tree) Q_ASSERT(m_upLink[id] == -1); Q_ASSERT(m_groupIds[id] != GroupType::Selection); bool regroup = true; // we don't support splitting if selection group is active // We do a BFS on the tree to copy it // We store corresponding nodes std::unordered_map corresp; // keys are id in the original tree, values are temporary negative id assigned for creation of the new tree corresp[-1] = -1; // These are the nodes to be moved to new tree std::vector to_move; // We store the groups (ie the nodes) that are going to be part of the new tree // Keys are temporary id (negative) and values are the set of children (true ids in the case of leaves and temporary ids for other nodes) std::unordered_map> new_groups; // We store also the target type of the new groups std::unordered_map new_types; std::queue queue; queue.push(id); int tempId = -10; while (!queue.empty()) { int current = queue.front(); queue.pop(); if (!isLeaf(current) || criterion(current)) { if (isLeaf(current)) { to_move.push_back(current); new_groups[corresp[m_upLink[current]]].insert(current); } else { corresp[current] = tempId; new_types[tempId] = getType(current); if (m_upLink[current] != -1) new_groups[corresp[m_upLink[current]]].insert(tempId); tempId--; } } for (const int &child : m_downLink.at(current)) { queue.push(child); } } // First, we simulate deletion of elements that we have to remove from the original tree // A side effect of this is that empty groups will be removed for (const auto &leaf : to_move) { - destructGroupItem(leaf, true, undo, redo); + destructGroupItem(leaf, true, undo, redo, list); } // we artificially recreate the leaves Fun operation = [this, to_move]() { for (const auto &leaf : to_move) { createGroupItem(leaf); } return true; }; Fun reverse = [this, to_move]() { for (const auto &group : to_move) { destructGroupItem(group); } return true; }; bool res = operation(); if (!res) { return false; } UPDATE_UNDO_REDO(operation, reverse, undo, redo); // We prune the new_groups to remove empty ones bool finished = false; while (!finished) { finished = true; int selected = INT_MAX; for (const auto &it : new_groups) { if (it.second.size() == 0) { // empty group finished = false; selected = it.first; break; } for (int it2 : it.second) { if (it2 < -1 && new_groups.count(it2) == 0) { // group that has no reference, it is empty too finished = false; selected = it2; break; } } if (!finished) break; } if (!finished) { new_groups.erase(selected); for (auto it = new_groups.begin(); it != new_groups.end(); ++it) { (*it).second.erase(selected); } } } // We now regroup the items of the new tree to recreate hierarchy. // This is equivalent to creating the tree bottom up (starting from the leaves) // At each iteration, we create a new node by grouping together elements that are either leaves or already created nodes. std::unordered_map created_id; // to keep track of node that we create while (!new_groups.empty()) { int selected = INT_MAX; for (const auto &group : new_groups) { // we check that all children are already created bool ok = true; for (int elem : group.second) { if (elem < -1 && created_id.count(elem) == 0) { ok = false; break; } } if (ok) { selected = group.first; break; } } Q_ASSERT(selected != INT_MAX); std::unordered_set group; for (int elem : new_groups[selected]) { group.insert(elem < -1 ? created_id[elem] : elem); } Q_ASSERT(new_types.count(selected) != 0); - int gid = groupItems(group, undo, redo, new_types[selected], true); + int gid = groupItems(group, undo, redo, list, new_types[selected], true); created_id[selected] = gid; new_groups.erase(selected); } if (regroup) { if (m_groupIds.count(id) > 0) { - mergeSingleGroups(id, undo, redo); + mergeSingleGroups(id, undo, redo, list); } if (created_id[corresp[id]]) { - mergeSingleGroups(created_id[corresp[id]], undo, redo); + mergeSingleGroups(created_id[corresp[id]], undo, redo, list); } } return res; } -void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo) +void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(targetId) > 0); Fun operation = [ this, id, group = m_upLink[targetId] ]() { setGroup(id, group); return true; }; Fun reverse = [ this, id, group = m_upLink[id] ]() { setGroup(id, group); return true; }; operation(); UPDATE_UNDO_REDO(operation, reverse, undo, redo); } -bool GroupsModel::createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo) +bool GroupsModel::createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(isLeaf(id)); if (to_add.size() == 0) { return true; } int gid = TimelineModel::getNextId(); std::unordered_map old_parents; to_add.insert(id); for (int g : to_add) { Q_ASSERT(m_upLink.count(g) > 0); old_parents[g] = m_upLink[g]; } Fun operation = [ this, id, gid, type, to_add, parent = m_upLink.at(id) ]() { createGroupItem(gid); setGroup(gid, parent); for (const auto &g : to_add) { setGroup(g, gid); } setType(gid, type); return true; }; Fun reverse = [this, id, old_parents, gid]() { for (const auto &g : old_parents) { setGroup(g.first, g.second); } setGroup(gid, -1); destructGroupItem_lambda(gid)(); return true; }; bool success = operation(); if (success) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return success; } -bool GroupsModel::processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo) +bool GroupsModel::processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo, Updates &list) { qDebug() << "processCopy" << gid; if (isLeaf(gid)) { qDebug() << "it is a leaf"; return true; } bool ok = true; std::unordered_set targetGroup; for (int child : m_downLink.at(gid)) { - ok = ok && processCopy(child, mapping, undo, redo); + ok = ok && processCopy(child, mapping, undo, redo, list); if (!ok) { break; } targetGroup.insert(mapping.at(child)); } qDebug() << "processCopy" << gid << "success of child" << ok; if (ok && m_groupIds[gid] != GroupType::Selection) { - int id = groupItems(targetGroup, undo, redo); + int id = groupItems(targetGroup, undo, redo, list); qDebug() << "processCopy" << gid << "created id" << id; if (id != -1) { mapping[gid] = id; return true; } } return ok; } -bool GroupsModel::copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo) +bool GroupsModel::copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo, Updates &list) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // destruct old groups for the targets items for (const auto &corresp : mapping) { - ungroupItem(corresp.second, local_undo, local_redo); + ungroupItem(corresp.second, local_undo, local_redo, list); } std::unordered_set roots; std::transform(mapping.begin(), mapping.end(), std::inserter(roots, roots.begin()), [&](decltype(*mapping.begin()) corresp) { return getRootId(corresp.first); }); bool res = true; qDebug() << "found" << roots.size() << "roots"; for (int r : roots) { qDebug() << "processing copy for root " << r; - res = res && processCopy(r, mapping, local_undo, local_redo); + res = res && processCopy(r, mapping, local_undo, local_redo, list); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } GroupType GroupsModel::getType(int id) const { if (m_groupIds.count(id) > 0) { return m_groupIds.at(id); } return GroupType::Leaf; } QJsonObject GroupsModel::toJson(int gid) const { QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(getType(gid)))); if (m_groupIds.count(gid) > 0) { // in that case, we have a proper group QJsonArray array; Q_ASSERT(m_downLink.count(gid) > 0); for (int c : m_downLink.at(gid)) { array.push_back(toJson(c)); } currentGroup.insert(QLatin1String("children"), array); } else { // in that case we have a clip or composition if (auto ptr = m_parent.lock()) { Q_ASSERT(ptr->isClip(gid) || ptr->isComposition(gid)); currentGroup.insert(QLatin1String("leaf"), QJsonValue(QLatin1String(ptr->isClip(gid) ? "clip" : "composition"))); int track = ptr->getTrackPosition(ptr->getItemTrackId(gid)); int pos = ptr->getItemPosition(gid); currentGroup.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(track).arg(pos))); } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } return currentGroup; } const QString GroupsModel::toJson() const { std::unordered_set roots; std::transform(m_groupIds.begin(), m_groupIds.end(), std::inserter(roots, roots.begin()), [&](decltype(*m_groupIds.begin()) g) { return getRootId(g.first); }); QJsonArray list; for (int r : roots) { if (getType(r) != GroupType::Selection) list.push_back(toJson(r)); } QJsonDocument json(list); return QString(json.toJson()); } -int GroupsModel::fromJson(const QJsonObject &o, Fun &undo, Fun &redo) +int GroupsModel::fromJson(const QJsonObject &o, Fun &undo, Fun &redo, Updates &list) { if (!o.contains(QLatin1String("type"))) { return -1; } auto type = groupTypeFromStr(o.value(QLatin1String("type")).toString()); if (type == GroupType::Leaf) { if (auto ptr = m_parent.lock()) { if (!o.contains(QLatin1String("data")) || !o.contains(QLatin1String("leaf"))) { qDebug() << "Error: missing info in the group structure while parsing json"; return -1; } QString data = o.value(QLatin1String("data")).toString(); QString leaf = o.value(QLatin1String("leaf")).toString(); int trackId = ptr->getTrackIndexFromPosition(data.section(":", 0, 0).toInt()); int pos = data.section(":", 1, 1).toInt(); int id = -1; if (leaf == QLatin1String("clip")) { id = ptr->getClipByPosition(trackId, pos); } else if (leaf == QLatin1String("composition")) { id = ptr->getCompositionByPosition(trackId, pos); } return id; } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } else { if (!o.contains(QLatin1String("children"))) { qDebug() << "Error: missing info in the group structure while parsing json"; return -1; } auto value = o.value(QLatin1String("children")); if (!value.isArray()) { qDebug() << "Error : Expected json array of children while parsing groups"; return -1; } const auto children = value.toArray(); std::unordered_set ids; for (const auto &c : children) { if (!c.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; return -1; } - ids.insert(fromJson(c.toObject(), undo, redo)); + ids.insert(fromJson(c.toObject(), undo, redo, list)); } if (ids.count(-1) > 0) { return -1; } - return groupItems(ids, undo, redo, type); + return groupItems(ids, undo, redo, list, type); } return -1; } bool GroupsModel::fromJson(const QString &data) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } - const auto list = json.array(); + const auto jarray = json.array(); bool ok = true; - for (const auto &elem : list) { + for (const auto &elem : jarray) { if (!elem.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; undo(); return false; } - ok = ok && fromJson(elem.toObject(), undo, redo); + ok = ok && fromJson(elem.toObject(), undo, redo, list); + if (ok) { + ModelUpdater::applyUpdates(undo, redo, list); + } } return ok; } void GroupsModel::setType(int gid, GroupType type) { Q_ASSERT(m_groupIds.count(gid) != 0); if (type == GroupType::Leaf) { Q_ASSERT(m_downLink[gid].size() == 0); if (m_groupIds.count(gid) > 0) { m_groupIds.erase(gid); } } else { m_groupIds[gid] = type; } } bool GroupsModel::checkConsistency(bool failOnSingleGroups, bool checkTimelineConsistency) { // check that all element with up link have a down link for (const auto &elem : m_upLink) { if (m_downLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } // check that all element with down link have a up link for (const auto &elem : m_downLink) { if (m_upLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } for (const auto &elem : m_upLink) { // iterate through children to check links for (const auto &child : m_downLink[elem.first]) { if (m_upLink[child] != elem.first) { qDebug() << "ERROR: Group model has inconsistent up/down links"; return false; } } bool isLeaf = m_downLink[elem.first].empty(); if (isLeaf) { if (m_groupIds.count(elem.first) > 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } } else { if (m_groupIds.count(elem.first) == 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } if (m_downLink[elem.first].size() == 1 && failOnSingleGroups) { qDebug() << "ERROR: Group model contains groups with single element"; return false; } if (elem.second != -1 && getType(elem.first) == GroupType::Selection) { qDebug() << "ERROR: Group model contains inner groups of selection type"; return false; } if (getType(elem.first) == GroupType::Leaf) { qDebug() << "ERROR: Group model contains groups of Leaf type"; return false; } } } // Finally, we do a depth first visit of the tree to check for loops std::unordered_set visited; for (const auto &elem : m_upLink) { if (elem.second == -1) { // this is a root, traverse the tree from here std::stack stack; stack.push(elem.first); while (!stack.empty()) { int cur = stack.top(); stack.pop(); if (visited.count(cur) > 0) { qDebug() << "ERROR: Group model contains a cycle"; return false; } visited.insert(cur); for (int child : m_downLink[cur]) { stack.push(child); } } } } // Do a last pass to check everybody was visited for (const auto &elem : m_upLink) { if (visited.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains unreachable elements"; return false; } } if (checkTimelineConsistency) { if (auto ptr = m_parent.lock()) { auto isTimelineObject = [&](int cid) { return ptr->isClip(cid) || ptr->isComposition(cid); }; for (int g : ptr->m_allGroups) { if (m_upLink.count(g) == 0 || getType(g) == GroupType::Leaf) { qDebug() << "ERROR: Timeline contains inconsistent group data"; return false; } } for (const auto &elem : m_upLink) { if (getType(elem.first) == GroupType::Leaf) { if (!isTimelineObject(elem.first)) { qDebug() << "ERROR: Group model contains leaf element that is not a clip nor a composition"; return false; } } else { if (ptr->m_allGroups.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains group element that is not registered on timeline"; Q_ASSERT(false); return false; } if (getType(elem.first) == GroupType::AVSplit) { if (m_downLink[elem.first].size() != 2) { qDebug() << "ERROR: Group model contains a AVSplit group with a children count != 2"; return false; } auto it = m_downLink[elem.first].begin(); int cid1 = (*it); ++it; int cid2 = (*it); if (!isTimelineObject(cid1) || !isTimelineObject(cid2)) { qDebug() << "ERROR: Group model contains an AVSplit group with invalid members"; return false; } int tid1 = ptr->getClipTrackId(cid1); bool isAudio1 = ptr->getTrackById(tid1)->isAudioTrack(); int tid2 = ptr->getClipTrackId(cid2); bool isAudio2 = ptr->getTrackById(tid2)->isAudioTrack(); if (isAudio1 == isAudio2) { qDebug() << "ERROR: Group model contains an AVSplit formed with members that are both on an audio track or on a video track"; return false; } } } } } } return true; } diff --git a/src/timeline2/model/groupsmodel.hpp b/src/timeline2/model/groupsmodel.hpp index 8dd44a7a4..cc1068c8f 100644 --- a/src/timeline2/model/groupsmodel.hpp +++ b/src/timeline2/model/groupsmodel.hpp @@ -1,231 +1,231 @@ /*************************************************************************** * 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 GROUPMODEL_H #define GROUPMODEL_H #include "definitions.h" +#include "modelupdater.hpp" #include "undohelper.hpp" #include #include #include #include class TimelineItemModel; /* @brief This class represents the group hiearchy. This is basically a tree structure In this class, we consider that a groupItem is either a clip or a group */ class GroupsModel { public: GroupsModel() = delete; GroupsModel(std::weak_ptr parent); /* @brief Create a group that contains all the given items and returns the id of the created group. Note that if an item is already part of a group, its topmost group will be considered instead and added in the newly created group. If only one id is provided, no group is created, unless force = true. @param ids set containing the items to group. @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation @param type indicates the type of group we create Returns the id of the new group, or -1 on error. */ - int groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal, bool force = false); + int groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, Updates &list, GroupType type = GroupType::Normal, bool force = false); protected: /* Lambda version */ Fun groupItems_lambda(int gid, const std::unordered_set &ids, GroupType type = GroupType::Normal, int parent = -1); public: /* Deletes the topmost group containing given element Note that if the element is not in a group, then it will not be touched. Return true on success @param id id of the groupitem @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool ungroupItem(int id, Fun &undo, Fun &redo); + bool ungroupItem(int id, Fun &undo, Fun &redo, Updates &list); /* @brief Create a groupItem in the hierarchy. Initially it is not part of a group @param id id of the groupItem */ void createGroupItem(int id); /* @brief Destruct a group item Note that this public function expects that the given id is an orphan element. @param id id of the groupItem */ bool destructGroupItem(int id); /* @brief Merges group with only one child to parent Ex: . . / \ / \ . . becomes a b / \ a b @param id id of the tree to consider */ - bool mergeSingleGroups(int id, Fun &undo, Fun &redo); + bool mergeSingleGroups(int id, Fun &undo, Fun &redo, Updates &list); /* @brief Split the group tree according to a given criterion All the leaves satisfying the criterion are moved to the new tree, the other stay Both tree are subsequently simplified to avoid weird structure. @param id is the root of the tree */ - bool split(int id, const std::function &criterion, Fun &undo, Fun &redo); + bool split(int id, const std::function &criterion, Fun &undo, Fun &redo, Updates &list); /* @brief Copy a group hierarchy. @param mapping describes the correspondence between the ids of the items in the source group hierarchy, and their counterpart in the hierarchy that we create. It will also be used as a return parameter, by adding the mapping between the groups of the hierarchy Note that if the target items should not belong to a group. */ - bool copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo); + bool copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo, Updates &list); /* @brief Get the overall father of a given groupItem If the element has no father, it is returned as is. @param id id of the groupitem */ int getRootId(int id) const; /* @brief Returns true if the groupItem has no descendant @param id of the groupItem */ bool isLeaf(int id) const; /* @brief Returns true if the element is in a non-trivial group @param id of the groupItem */ bool isInGroup(int id) const; /* @brief Move element id in the same group as targetId */ - void setInGroupOf(int id, int targetId, Fun &undo, Fun &redo); + void setInGroupOf(int id, int targetId, Fun &undo, Fun &redo, Updates &list); /* @brief We replace the leaf node given by id with a group that contains the leaf plus all the clips in to_add. * The created group type is given in parameter * Returns true on success */ - bool createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo); + bool createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo, Updates &list); /* @brief Returns the id of all the descendant of given item (including item) @param id of the groupItem */ std::unordered_set getSubtree(int id) const; /* @brief Returns the id of all the leaves in the subtree of the given item This should correspond to the ids of the clips, since they should be the only items with no descendants @param id of the groupItem */ std::unordered_set getLeaves(int id) const; /* @brief Gets direct children of a given group item @param id of the groupItem */ std::unordered_set getDirectChildren(int id) const; /* @brief Gets direct ancestor of a given group item. Returns -1 if not in a group @param id of the groupItem */ int getDirectAncestor(int id) const; /* @brief Get the type of the group @param id of the groupItem. Must be a proper group, not a leaf */ GroupType getType(int id) const; /* @brief Convert the group hierarchy to json. Note that we cannot expect clipId nor groupId to be the same on project reopening, thus we cannot rely on them for saving. To workaround that, we currently identify clips by their position + track */ const QString toJson() const; bool fromJson(const QString &data); /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getSplitPartner(int id) const; /* @brief Check the internal consistency of the model. Returns false if something is wrong @param failOnSingleGroups: if true, we make sure that a non-leaf node has at least two children @param checkTimelineConsistency: if true, we make sure that the group data of the parent timeline are consistent */ bool checkConsistency(bool failOnSingleGroups = true, bool checkTimelineConsistency = false); protected: /* @brief Destruct a groupItem in the hierarchy. All its children will become their own roots Return true on success @param id id of the groupitem @param deleteOrphan If this parameter is true, we recursively delete any group that become empty following the destruction @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo); + bool destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo, Updates &list); /* Lambda version */ Fun destructGroupItem_lambda(int id); /* @brief change the group of a given item @param id of the groupItem @param groupId id of the group to assign it to - @param changeState when false, the grouped role for item won't be updated (for selection) */ - void setGroup(int id, int groupId, bool changeState = true); + void setGroup(int id, int groupId); /* @brief Remove an item from all the groups it belongs to. @param id of the groupItem */ void removeFromGroup(int id); /* @brief This is the actual recursive implementation of the copy function. */ - bool processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo); + bool processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo, Updates &list); /* @brief This is the actual recursive implementation of the conversion to json */ QJsonObject toJson(int gid) const; /* @brief This is the actual recursive implementation of the parsing from json Returns the id of the created group */ - int fromJson(const QJsonObject &o, Fun &undo, Fun &redo); + int fromJson(const QJsonObject &o, Fun &undo, Fun &redo, Updates &list); /* @brief Transform a leaf node into a group node of given type. This implies doing the registration to the timeline */ void promoteToGroup(int gid, GroupType type); /* @brief Transform a group node with no children into a leaf. This implies doing the deregistration to the timeline */ void downgradeToLeaf(int gid); /* @Brief helper function to change the type of a group. @param id of the groupItem @param type: new type of the group */ void setType(int gid, GroupType type); private: std::weak_ptr m_parent; std::unordered_map m_upLink; // edges toward parent std::unordered_map> m_downLink; // edges toward children std::unordered_map m_groupIds; // this keeps track of "real" groups (non-leaf elements), and their types mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access }; #endif diff --git a/src/timeline2/model/modelupdater.cpp b/src/timeline2/model/modelupdater.cpp new file mode 100644 index 000000000..0c6c269eb --- /dev/null +++ b/src/timeline2/model/modelupdater.cpp @@ -0,0 +1,743 @@ +/*************************************************************************** + * Copyright (C) 2018 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 "modelupdater.hpp" +#include "timelinemodel.hpp" +#include "trackmodel.hpp" +#include + +namespace { +// This function assumes all the updates in the list correspond to the same item +// It will merge move update together. We also merge insert + move in an insert to the right position +std::vector> mergeMoves(const std::vector> &list) +{ + if (list.size() == 0) { + return {}; + } + std::vector> result; + int itemId = list[0]->getItemId(); + + int sourceTrackId = -1; + int sourcePos = -1; + std::weak_ptr sourceTimeline; + int targetTrackId = -1; + int targetPos = -1; + std::weak_ptr targetTimeline; + + bool firstMoveFound = false; + bool isOperationInsert = false; + bool isClip = true; + + for (const auto &update : list) { + Q_ASSERT(update->getItemId() == itemId); + if (!update->isMove()) { + if (update->isInsert()) { + if (firstMoveFound) { + // impossible to have an insert on something already inserted... + Q_ASSERT(false); + } else { + isOperationInsert = true; + auto ins = std::static_pointer_cast(update); + targetPos = ins->getPos(); + targetTrackId = ins->getTrackId(); + targetTimeline = ins->getTimeline(); + isClip = ins->isClip(); + } + } else if (update->isDelete() && firstMoveFound) { + // move then delete should have been merged into delete at this point. + Q_ASSERT(false); + } else { + result.push_back(update); + } + } else { + firstMoveFound = true; + auto move = std::static_pointer_cast(update); + if (sourcePos == -1) { + sourcePos = move->getSourcePos(); + sourceTrackId = move->getSourceTrackId(); + sourceTimeline = move->getSourceTimeline(); + targetPos = move->getTargetPos(); + targetTrackId = move->getTargetTrackId(); + targetTimeline = move->getTargetTimeline(); + } else { + targetPos = move->getTargetPos(); + targetTrackId = move->getTargetTrackId(); + targetTimeline = move->getTargetTimeline(); + } + } + } + + if (isOperationInsert) { + result.emplace_back(new InsertUpdate(itemId, targetTimeline, targetTrackId, targetPos, isClip)); + } else if (sourcePos != -1) { + // we do have a move, merge + if (sourceTimeline.lock() == targetTimeline.lock() && sourceTrackId == targetTrackId) { + // same track move, replace with change + result.emplace_back(new ChangeUpdate(itemId, sourceTimeline, {TimelineModel::StartRole})); + } else { + result.emplace_back(new MoveUpdate(itemId, sourceTimeline, sourceTrackId, sourcePos, targetTimeline, targetTrackId, targetPos)); + } + } + return result; +} + +// This function assumes all the updates in the list correspond to the same item, and that at this point we don't have any moves +// It will merge pairs of delete + insert into a move +// It also cleans the following situations: +// - Last operation is delete: all the previous ones can be safely discarded +// - First operation is insert, last one is delete: that is equivalent to doing nothing +std::vector> mergeDeleteInsert(const std::vector> &list) +{ + if (list.size() == 0) { + return {}; + } + std::vector> result; + int itemId = list[0]->getItemId(); + + int sourceTrackId = -1; + int sourcePos = -1; + bool isClip = true; + std::weak_ptr sourceTimeline; + + for (const auto &update : list) { + Q_ASSERT(update->getItemId() == itemId); + Q_ASSERT(!update->isMove()); + if (update->isDelete() && sourceTrackId == -1) { + auto del = std::static_pointer_cast(update); + sourceTrackId = del->getTrackId(); + sourcePos = del->getPos(); + sourceTimeline = del->getTimeline(); + isClip = del->isClip(); + Q_ASSERT(sourceTrackId != -1); + } else if (sourceTrackId != -1 && update->isInsert()) { + auto insert = std::static_pointer_cast(update); + result.emplace_back( + new MoveUpdate(itemId, sourceTimeline, sourceTrackId, sourcePos, insert->getTimeline(), insert->getTrackId(), insert->getPos())); + sourceTrackId = -1; + sourcePos = -1; + isClip = insert->isClip(); + } else if (sourceTrackId != -1 && update->isDelete()) { + // we found a double delete, problematic... + Q_ASSERT(false); + } else { + result.push_back(update); + } + } + + if (sourcePos != -1) { + // If we reach this, it means that the last operation was a delete, but we didn't find any subsequent insert. + // That means we can remove any update before the delete, since they are not doing anything + + // If the first operation is an insert, it means that overall this sequence inserts then deletes, which is equivalent to doing nothing. + std::vector> result_clean; + + bool isFirstOperationInsert = false; + for (const auto &update : result) { + if (update->isDelete() || update->isInsert() || update->isMove()) { + isFirstOperationInsert = update->isInsert(); + break; + } + } + if (!isFirstOperationInsert) { + result_clean.emplace_back(new DeleteUpdate(itemId, sourceTimeline, sourceTrackId, sourcePos, isClip)); + } + } + return result; +} + +// This function assumes all the updates in the list correspond to the same item +// It will merge change updates together. If changes are constructed with different timeline pointers, this is a bit problematic, it means that the item was +// moved to a different timeline along the way. We track the insert/moves to check what is the last timeline it was seen in. This function must be called after +// move/insert/delete filters. That means that there is at most one move/insert/delete operation is the update list +std::vector> mergeChanges(const std::vector> &list) +{ + qDebug() << "merging changes"; + if (list.size() == 0) { + return {}; + } + std::vector> result; + int itemId = list[0]->getItemId(); + std::unordered_set roles; + bool seenMove = false; + std::weak_ptr timeline; + + for (const auto &update : list) { + Q_ASSERT(update->getItemId() == itemId); + update->print(); + if (update->isDelete()) { + // in case TimelineModel::of a delete, we don't care about changes. + std::vector> res; + std::copy_if(list.begin(), list.end(), std::back_inserter(res), [](const std::shared_ptr &u) { return !u->isChange(); }); + qDebug() << "found delete, aborting"; + return res; + } else if (update->isInsert()) { + Q_ASSERT(!seenMove); + auto insert = std::static_pointer_cast(update); + timeline = insert->getTimeline(); + seenMove = true; + result.push_back(update); + } else if (update->isMove()) { + Q_ASSERT(!seenMove); + auto move = std::static_pointer_cast(update); + timeline = move->getTargetTimeline(); + seenMove = true; + result.push_back(update); + } else if (update->isChange()) { + auto change = std::static_pointer_cast(update); + auto curRoles = change->getRoles(); + roles.insert(curRoles.begin(), curRoles.end()); + if (!seenMove) { + timeline = change->getTimeline(); + } + } else { + // not implemented? + Q_ASSERT(false); + } + } + if (roles.size() > 0) { + QVector rolesVec; + rolesVec.reserve((int)roles.size()); + for (int role : roles) { + rolesVec.push_back(role); + } + result.push_back(std::make_shared(itemId, timeline, rolesVec)); + } + return result; +} +// This function cleans a list of updates to be applied. +// After this function is applied, each item should have at most one insert/delete or move operation +std::vector> simplify(const std::vector> &list) +{ + qDebug() << "starting to simplify updates"; + + // First, we simply regroup updates by item + std::unordered_map>> updatesByItem; + + for (const auto &u : list) { + updatesByItem[u->getItemId()].push_back(u); + } + + // Then, we simplify updates element by element + + std::vector> res; + for (const auto &u : updatesByItem) { + auto curated = mergeChanges(mergeMoves(mergeDeleteInsert(u.second))); + res.insert(res.end(), curated.begin(), curated.end()); + } + + // Finally, we sort, in order to move the ChangeUpdates to the end + std::sort(res.begin(), res.end(), [](const auto &a, const auto &b) { + if (a->isChange() == b->isChange()) { + return false; + } + return !a->isChange(); + }); + return res; +} + +// This function takes a simplified list of updates and compute the reverse operations +std::vector> reverse(const std::vector> &list) +{ +#ifdef QT_DEBUG + // in debug mode, we check whether the actions are indeed simplified + std::unordered_set idSeenChange, idSeenMove; +#endif + + std::vector> res; + for (int i = (int)list.size() - 1; i >= 0; --i) { + std::shared_ptr update = list[size_t(i)]; + res.emplace_back(update->reverse()); +#ifdef QT_DEBUG + update->print(); + + if (update->isChange()) { + if (idSeenChange.count(update->getItemId()) > 0) { + Q_ASSERT(false); + } + idSeenChange.insert(update->getItemId()); + } else { + if (idSeenMove.count(update->getItemId()) > 0) { + Q_ASSERT(false); + } + idSeenMove.insert(update->getItemId()); + } +#endif + } + return res; +} +} // namespace + +// This creates a lambda to be executed before the operation to prepare the model for update +Fun ModelUpdater::preApply_lambda(const std::vector> &list) +{ + auto getCurrentRow = [](std::shared_ptr timeline, int trackId, int itemId) { + int row = -1; + if (timeline->isClip(itemId)) { + row = timeline->getTrackById(trackId)->getRowfromClip(itemId); + } else { + Q_ASSERT(timeline->isComposition(itemId)); + row = timeline->getTrackById(trackId)->getRowfromComposition(itemId); + } + return row; + }; + auto getTentativeRow = [](std::shared_ptr timeline, int trackId, int itemId, bool isClip) { + int row = -1; + if (isClip) { + row = timeline->getTrackById(trackId)->getTentativeRowfromClip(itemId); + } else { + row = timeline->getTrackById(trackId)->getTentativeRowfromComposition(itemId); + } + return row; + }; + return [ list, getCurrentRow = std::move(getCurrentRow), getTentativeRow = std::move(getTentativeRow) ]() + { + for (const auto &u : list) { + if (u->isDelete()) { + auto del = std::static_pointer_cast(u); + if (auto timeline = del->getTimeline().lock()) { + int trackId = del->getTrackId(), itemId = del->getItemId(); + int row = getCurrentRow(timeline, trackId, itemId); + timeline->_beginRemoveRows(timeline->makeTrackIndexFromID(trackId), row, row); + } else { + qDebug() << "ERROR: impossible to lock timeline"; + Q_ASSERT(false); + } + } else if (u->isInsert()) { + auto ins = std::static_pointer_cast(u); + if (auto timeline = ins->getTimeline().lock()) { + int trackId = ins->getTrackId(), itemId = ins->getItemId(); + int row = getTentativeRow(timeline, trackId, itemId, ins->isClip()); + timeline->_beginInsertRows(timeline->makeTrackIndexFromID(trackId), row, row); + } else { + qDebug() << "ERROR: impossible to lock timeline"; + Q_ASSERT(false); + } + } else if (u->isMove()) { + auto move = std::static_pointer_cast(u); + if (auto sTimeline = move->getSourceTimeline().lock()) { + if (auto tTimeline = move->getTargetTimeline().lock()) { + int sTrackId = move->getSourceTrackId(), itemId = move->getItemId(); + int tTrackId = move->getTargetTrackId(); + int sRow = getCurrentRow(sTimeline, sTrackId, itemId); + int tRow = getTentativeRow(tTimeline, tTrackId, itemId, sTimeline->isClip(itemId)); + if (sTimeline == tTimeline) { + // we have a "true" move, within the timeline + sTimeline->_beginMoveRows(sTimeline->makeTrackIndexFromID(sTrackId), sRow, sRow, tTimeline->makeTrackIndexFromID(tTrackId), tRow); + } else { + // we move to a different timeline, we need to delete + insert + sTimeline->_beginRemoveRows(sTimeline->makeTrackIndexFromID(sTrackId), sRow, sRow); + tTimeline->_beginInsertRows(tTimeline->makeTrackIndexFromID(tTrackId), tRow, tRow); + } + } else { + qDebug() << "ERROR: impossible to lock target timeline"; + Q_ASSERT(false); + } + } else { + qDebug() << "ERROR: impossible to lock source timeline"; + Q_ASSERT(false); + } + } else if (u->isChange()) { + // nothing to do here, will be done in post apply + } else { + // not implemented? + Q_ASSERT(false); + } + } + return true; + }; +} + +// This creates a lambda to be executed after the operation to finalize the update +Fun ModelUpdater::postApply_lambda(const std::vector> &list) +{ + return [list]() { + for (const auto &u : list) { + if (u->isDelete()) { + auto del = std::static_pointer_cast(u); + if (auto timeline = del->getTimeline().lock()) { + timeline->_endRemoveRows(); + } else { + qDebug() << "ERROR: impossible to lock timeline"; + Q_ASSERT(false); + } + } else if (u->isInsert()) { + auto ins = std::static_pointer_cast(u); + if (auto timeline = ins->getTimeline().lock()) { + timeline->_endInsertRows(); + } else { + qDebug() << "ERROR: impossible to lock timeline"; + Q_ASSERT(false); + } + } else if (u->isMove()) { + auto move = std::static_pointer_cast(u); + if (auto sTimeline = move->getSourceTimeline().lock()) { + if (auto tTimeline = move->getTargetTimeline().lock()) { + if (sTimeline == tTimeline) { + sTimeline->_endMoveRows(); + } else { + sTimeline->_endRemoveRows(); + tTimeline->_endInsertRows(); + } + } else { + qDebug() << "ERROR: impossible to lock target timeline"; + Q_ASSERT(false); + } + } else { + qDebug() << "ERROR: impossible to lock source timeline"; + Q_ASSERT(false); + } + } else if (u->isChange()) { + auto change = std::static_pointer_cast(u); + if (auto timeline = change->getTimeline().lock()) { + int itemId = change->getItemId(); + QModelIndex idx; + if (timeline->isClip(itemId)) { + idx = timeline->makeClipIndexFromID(itemId); + } else { + Q_ASSERT(timeline->isComposition(itemId)); + idx = timeline->makeCompositionIndexFromID(itemId); + } + timeline->notifyChange(idx, idx, change->getRoles()); + } + } else { + // not implemented? + Q_ASSERT(false); + } + } + return true; + }; +} + +void ModelUpdater::applyUpdates(Fun &undo, Fun &redo, const std::vector> &list) +{ + if (list.size() == 0) { + // Nothing todo, we can pass + return; + } + auto updates = simplify(list); + auto rev_updates = reverse(updates); + + // firstly, we need to undo the action + bool undone = undo(); + Q_ASSERT(undone); + + redo = [ redo, pre = preApply_lambda(updates), post = postApply_lambda(updates) ]() + { + pre(); + auto res = redo(); + post(); + return res; + }; + undo = [ undo, pre = preApply_lambda(rev_updates), post = postApply_lambda(rev_updates) ]() + { + pre(); + auto res = undo(); + post(); + return res; + }; + redo(); +} + +DeleteUpdate::DeleteUpdate(int itemId, std::weak_ptr timeline, int trackId, int pos, bool isClip) + : m_timeline(timeline) + , m_itemId(itemId) + , m_trackId(trackId) + , m_pos(pos) + , m_isClip(isClip) +{ +} +bool DeleteUpdate::isDelete() const +{ + return true; +} +int DeleteUpdate::getItemId() const +{ + return m_itemId; +} +int DeleteUpdate::getTrackId() const +{ + return m_trackId; +} +int DeleteUpdate::getPos() const +{ + return m_pos; +} +bool DeleteUpdate::isClip() const +{ + return m_isClip; +} +std::weak_ptr DeleteUpdate::getTimeline() const +{ + return m_timeline; +} + +std::shared_ptr DeleteUpdate::reverse() +{ + return std::shared_ptr(new InsertUpdate(m_itemId, m_timeline, m_trackId, m_pos, m_isClip)); +} + +void DeleteUpdate::print() const +{ + qDebug() << "Delete Update of item " << m_itemId << "from track" << m_trackId << "at pos" << m_pos; +} + +InsertUpdate::InsertUpdate(int itemId, std::weak_ptr timeline, int trackId, int pos, bool isClip) + : m_timeline(timeline) + , m_itemId(itemId) + , m_trackId(trackId) + , m_pos(pos) + , m_isClip(isClip) +{ +} +bool InsertUpdate::isInsert() const +{ + return true; +} +int InsertUpdate::getItemId() const +{ + return m_itemId; +} +int InsertUpdate::getTrackId() const +{ + return m_trackId; +} +int InsertUpdate::getPos() const +{ + return m_pos; +} +bool InsertUpdate::isClip() const +{ + return m_isClip; +} +std::weak_ptr InsertUpdate::getTimeline() const +{ + return m_timeline; +} + +std::shared_ptr InsertUpdate::reverse() +{ + return std::shared_ptr(new DeleteUpdate(m_itemId, m_timeline, m_trackId, m_pos, m_isClip)); +} + +void InsertUpdate::print() const +{ + qDebug() << "Insert Update of item " << m_itemId << "to track" << m_trackId << "at pos" << m_pos; +} + +MoveUpdate::MoveUpdate(int itemId, std::weak_ptr sourceTimeline, int sourceTrackId, int sourcePos, std::weak_ptr targetTimeline, + int targetTrackId, int targetPos) + : m_itemId(itemId) + , m_sourceTimeline(sourceTimeline) + , m_sourceTrackId(sourceTrackId) + , m_sourcePos(sourcePos) + , m_targetTimeline(targetTimeline) + , m_targetTrackId(targetTrackId) + , m_targetPos(targetPos) +{ +} +bool MoveUpdate::isMove() const +{ + return true; +} +int MoveUpdate::getItemId() const +{ + return m_itemId; +} +std::weak_ptr MoveUpdate::getSourceTimeline() const +{ + return m_sourceTimeline; +} +std::weak_ptr MoveUpdate::getTargetTimeline() const +{ + return m_targetTimeline; +} +int MoveUpdate::getSourceTrackId() const +{ + return m_sourceTrackId; +} +int MoveUpdate::getSourcePos() const +{ + return m_sourcePos; +} +int MoveUpdate::getTargetTrackId() const +{ + return m_targetTrackId; +} +int MoveUpdate::getTargetPos() const +{ + return m_targetPos; +} + +std::shared_ptr MoveUpdate::reverse() +{ + return std::make_shared(m_itemId, m_targetTimeline, m_targetTrackId, m_targetPos, m_targetTimeline, m_targetTrackId, m_targetPos); +} + +void MoveUpdate::print() const +{ + qDebug() << "Move Update of item " << m_itemId << "from track" << m_sourceTrackId << "at pos" << m_sourcePos << "to track" << m_targetTrackId << "at pos" + << m_targetPos; +} + +ChangeUpdate::ChangeUpdate(int itemId, std::weak_ptr timeline, const QVector &roles) + : m_timeline(timeline) + , m_itemId(itemId) + , m_roles(roles) +{ +} +bool ChangeUpdate::isChange() const +{ + return true; +} +int ChangeUpdate::getItemId() const +{ + return m_itemId; +} +QVector ChangeUpdate::getRoles() const +{ + return m_roles; +} +std::weak_ptr ChangeUpdate::getTimeline() const +{ + return m_timeline; +} + +std::shared_ptr ChangeUpdate::reverse() +{ + return std::make_shared(m_itemId, m_timeline, m_roles); +} + +void ChangeUpdate::print() const +{ + qDebug() << "Change Update of item " << m_itemId << "roles:"; + for (int role : m_roles) { + switch (role) { + case TimelineModel::NameRole: + qDebug() << "NameRole"; + break; + case TimelineModel::ResourceRole: + qDebug() << "ResourceRole"; + break; + case TimelineModel::ServiceRole: + qDebug() << "ServiceRole"; + break; + case TimelineModel::IsBlankRole: + qDebug() << "IsBlankRole"; + break; + case TimelineModel::StartRole: + qDebug() << "StartRole"; + break; + case TimelineModel::BinIdRole: + qDebug() << "BinIdRole"; + break; + case TimelineModel::MarkersRole: + qDebug() << "MarkersRole"; + break; + case TimelineModel::StatusRole: + qDebug() << "StatusRole"; + break; + case TimelineModel::TypeRole: + qDebug() << "TypeRole"; + break; + case TimelineModel::KeyframesRole: + qDebug() << "KeyframesRole"; + break; + case TimelineModel::DurationRole: + qDebug() << "DurationRole"; + break; + case TimelineModel::InPointRole: + qDebug() << "InPointRole"; + break; + case TimelineModel::OutPointRole: + qDebug() << "OutPointRole"; + break; + case TimelineModel::FramerateRole: + qDebug() << "FramerateRole"; + break; + case TimelineModel::GroupedRole: + qDebug() << "GroupedRole"; + break; + case TimelineModel::HasAudio: + qDebug() << "HasAudio"; + break; + case TimelineModel::CanBeAudioRole: + qDebug() << "CanBeAudioRole"; + break; + case TimelineModel::CanBeVideoRole: + qDebug() << "CanBeVideoRole"; + break; + case TimelineModel::IsDisabledRole: + qDebug() << "IsDisabledRole"; + break; + case TimelineModel::IsAudioRole: + qDebug() << "IsAudioRole"; + break; + case TimelineModel::SortRole: + qDebug() << "SortRole"; + break; + case TimelineModel::ShowKeyframesRole: + qDebug() << "ShowKeyframesRole"; + break; + case TimelineModel::AudioLevelsRole: + qDebug() << "AudioLevelsRole"; + break; + case TimelineModel::IsCompositeRole: + qDebug() << "IsCompositeRole"; + break; + case TimelineModel::IsLockedRole: + qDebug() << "IsLockedRole"; + break; + case TimelineModel::HeightRole: + qDebug() << "HeightRole"; + break; + case TimelineModel::TrackTagRole: + qDebug() << "TrackTagRole"; + break; + case TimelineModel::FadeInRole: + qDebug() << "FadeInRole"; + break; + case TimelineModel::FadeOutRole: + qDebug() << "FadeOutRole"; + break; + case TimelineModel::IsCompositionRole: + qDebug() << "IsCompositionRole"; + break; + case TimelineModel::FileHashRole: + qDebug() << "FileHashRole"; + break; + case TimelineModel::SpeedRole: + qDebug() << "SpeedRole"; + break; + case TimelineModel::ReloadThumbRole: + qDebug() << "ReloadThumbRole"; + break; + case TimelineModel::ItemATrack: + qDebug() << "ItemATrack"; + break; + case TimelineModel::ItemIdRole: + qDebug() << "ItemIdRole"; + break; + case TimelineModel::ThumbsFormatRole: + qDebug() << "ThumbsFormatRole"; + break; + } + } +} diff --git a/src/timeline2/model/modelupdater.hpp b/src/timeline2/model/modelupdater.hpp new file mode 100644 index 000000000..7c94b073f --- /dev/null +++ b/src/timeline2/model/modelupdater.hpp @@ -0,0 +1,148 @@ +/*************************************************************************** + * Copyright (C) 2018 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 MODELUPDATER_H +#define MODELUPDATER_H + +#include "undohelper.hpp" +#include +#include +#include + +/* @brief + +*/ +class TimelineModel; + +class AbstractUpdate +{ +public: + AbstractUpdate() {} + + virtual ~AbstractUpdate() = default; + + virtual std::shared_ptr reverse() = 0; + + virtual int getItemId() const = 0; + virtual bool isDelete() const { return false; } + virtual bool isInsert() const { return false; } + virtual bool isChange() const { return false; } + virtual bool isMove() const { return false; } + + // for debug purposes + virtual void print() const = 0; +}; + +class DeleteUpdate : public AbstractUpdate +{ +public: + DeleteUpdate(int itemId, std::weak_ptr timeline, int trackId, int pos, bool isClip); + bool isDelete() const override; + int getItemId() const; + int getTrackId() const; + int getPos() const; + bool isClip() const; + std::weak_ptr getTimeline() const; + + std::shared_ptr reverse() override; + void print() const override; + +protected: + std::weak_ptr m_timeline; + int m_itemId, m_trackId, m_pos; + bool m_isClip; +}; + +class InsertUpdate : public AbstractUpdate +{ +public: + InsertUpdate(int itemId, std::weak_ptr timeline, int trackId, int pos, bool isClip); + bool isInsert() const override; + int getItemId() const; + int getTrackId() const; + int getPos() const; + bool isClip() const; + std::weak_ptr getTimeline() const; + + std::shared_ptr reverse() override; + void print() const override; + +protected: + std::weak_ptr m_timeline; + int m_itemId, m_trackId, m_pos; + bool m_isClip; +}; + +class MoveUpdate : public AbstractUpdate +{ +public: + MoveUpdate(int itemId, std::weak_ptr sourceTimeline, int sourceTrackId, int sourcePos, std::weak_ptr targetTimeline, + int targetTrackId, int targetPos); + bool isMove() const override; + int getItemId() const; + std::weak_ptr getSourceTimeline() const; + std::weak_ptr getTargetTimeline() const; + int getSourceTrackId() const; + int getSourcePos() const; + int getTargetTrackId() const; + int getTargetPos() const; + + std::shared_ptr reverse() override; + void print() const override; + +protected: + int m_itemId; + std::weak_ptr m_sourceTimeline; + int m_sourceTrackId, m_sourcePos; + std::weak_ptr m_targetTimeline; + int m_targetTrackId, m_targetPos; +}; + +class ChangeUpdate : public AbstractUpdate +{ +public: + ChangeUpdate(int itemId, std::weak_ptr timeline, const QVector &roles); + bool isChange() const override; + int getItemId() const; + QVector getRoles() const; + std::weak_ptr getTimeline() const; + + std::shared_ptr reverse() override; + void print() const override; + +protected: + std::weak_ptr m_timeline; + int m_itemId; + QVector m_roles; +}; + +using Updates = std::vector>; +class ModelUpdater +{ +public: + static void applyUpdates(Fun &undo, Fun &redo, const std::vector> &list); + +protected: + static Fun preApply_lambda(const std::vector> &list); + static Fun postApply_lambda(const std::vector> &list); +}; + +#endif diff --git a/src/timeline2/model/moveableItem.hpp b/src/timeline2/model/moveableItem.hpp index c94a25a0e..b3183a243 100644 --- a/src/timeline2/model/moveableItem.hpp +++ b/src/timeline2/model/moveableItem.hpp @@ -1,124 +1,124 @@ /*************************************************************************** * 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 MOVEABLEITEM_H #define MOVEABLEITEM_H #include "timelinemodel.hpp" #include "trackmodel.hpp" #include "undohelper.hpp" #include #include #include /* @brief This is the base class for objects that can move, for example clips and compositions */ template class MoveableItem { MoveableItem() = delete; protected: virtual ~MoveableItem() {} public: MoveableItem(std::weak_ptr parent, int id = -1); /* @brief returns (unique) id of current item */ int getId() const; /* @brief returns the length of the item on the timeline */ virtual int getPlaytime() const = 0; /* @brief returns the id of the track in which this items is inserted (-1 if none) */ int getCurrentTrackId() const; /* @brief returns the current position of the item (-1 if not inserted) */ int getPosition() const; /* @brief returns the in and out times of the item */ std::pair getInOut() const; int getIn() const; int getOut() const; friend class TrackModel; friend class TimelineModel; /* Implicit conversion operator to access the underlying producer */ operator Service &() { return *service(); } /* Returns true if the underlying producer is valid */ bool isValid(); /* @brief returns a property of the current item */ virtual const QString getProperty(const QString &name) const = 0; /* Set if the item is in grab state */ bool isGrabbed() const; void setGrab(bool grab); protected: /* @brief Returns a pointer to the service. It may be used but do NOT store it*/ virtual Service *service() const = 0; /* @brief Performs a resize of the given item. Returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. If a snap point is within reach, the operation will be coerced to use it. @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - virtual bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) = 0; + virtual bool requestResize(int size, bool right, Fun &undo, Fun &redo, Updates &list, bool logUndo = true) = 0; /* Updates the stored position of the item This function is meant to be called by the trackmodel, not directly by the user. If you whish to actually move the item, use the requestMove slot. */ void setPosition(int position); /* Updates the stored track id of the item This function is meant to be called by the timeline, not directly by the user. If you whish to actually change the track the item, use the slot in the timeline slot. */ void setCurrentTrackId(int tid); /* Set in and out of service */ virtual void setInOut(int in, int out); protected: std::weak_ptr m_parent; int m_id; // this is the creation id of the item, used for book-keeping int m_position; int m_currentTrackId; bool m_grabbed; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access }; #include "moveableItem.ipp" #endif diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index cccb4e060..914ab1a98 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,590 +1,617 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle 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 "timelinefunctions.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include #include -bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo, + Updates &list) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); - bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, undo, redo); + bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, undo, redo, list); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // Apply speed effect if necessary if (!qFuzzyCompare(clipSpeed, 1.0)) { - timeline->m_allClips[newId]->useTimewarpProducer(clipSpeed, undo, redo); + timeline->m_allClips[newId]->useTimewarpProducer(clipSpeed, undo, redo, list); } // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); - res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); - res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); + res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo, list); + res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo, list); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack); return res; } bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; for (const QString &binId : binIds) { int clipId; - if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, true, undo, redo)) { + if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, true, undo, redo, list)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } + ModelUpdater::applyUpdates(undo, redo, list); if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } -bool TimelineFunctions::processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) +bool TimelineFunctions::processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo, Updates &list) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); - bool res = copyClip(timeline, clipId, newId, state, undo, redo); - res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); + bool res = copyClip(timeline, clipId, newId, state, undo, redo, list); + res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo, list); int newDuration = timeline->getClipPlaytime(clipId); - res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); + res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo, list); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); - res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo); + res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo, list); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); + Updates list; + bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo, list); + ModelUpdater::applyUpdates(undo, redo, list); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } -bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo) +bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo, Updates &list) { const std::unordered_set clips = timeline->getGroupElements(clipId); int root = timeline->m_groups->getRootId(clipId); std::unordered_set topElements; if (timeline->m_temporarySelectionGroup == root) { topElements = timeline->m_groups->getDirectChildren(root); } else { topElements.insert(root); } // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) pCore->clearSelection(); int count = 0; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { count++; int newId; - bool res = processClipCut(timeline, cid, position, newId, undo, redo); + bool res = processClipCut(timeline, cid, position, newId, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } // splitted elements go temporarily in the same group as original ones. - timeline->m_groups->setInGroupOf(newId, cid, undo, redo); + timeline->m_groups->setInGroupOf(newId, cid, undo, redo, list); } } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hiearchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; bool res = true; for (const int topId : topElements) { - res = res & timeline->m_groups->split(topId, criterion, undo, redo); + res = res & timeline->m_groups->split(topId, criterion, undo, redo, list); } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1); if (clips.size() > 0) { timeline->requestClipsGroup(clips, false, GroupType::Selection); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(clipId); timeline->requestClipMove(clipId, track, startPosition, false, false); std::unordered_set clips = timeline->getGroupElements(clipId); // break group pCore->clearSelection(); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; - int res = timeline->requestClipsGroup(clips, undo, redo); - bool final = false; + Updates list; + int res = timeline->requestClipsGroup(clips, undo, redo, list); + bool isFinal = false; if (res > -1) { if (clips.size() > 1) { - final = timeline->requestGroupMove(clipId, res, 0, endPosition - startPosition, true, true, undo, redo); + isFinal = timeline->requestGroupMove(clipId, res, 0, endPosition - startPosition, true, true, undo, redo, list); } else { // only 1 clip to be moved - final = timeline->requestClipMove(clipId, track, endPosition, true, true, undo, redo); + isFinal = timeline->requestClipMove(clipId, track, endPosition, true, true, undo, redo, list); } } - if (final && clips.size() > 1) { - final = timeline->requestClipUngroup(clipId, undo, redo); + if (isFinal && clips.size() > 1) { + isFinal = timeline->requestClipUngroup(clipId, undo, redo, list); } - if (final) { + if (isFinal) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Insert space")); return true; } return false; } bool TimelineFunctions::extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; bool result = true; for (int trackId : tracks) { - result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); + result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo, list); } if (result && !liftOnly) { - result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo); + result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo, list); + } + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); } - pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; bool result = false; if (overwrite) { - result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); + result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, list); } else { // Cut all tracks auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->isLocked()) { ++it; continue; } int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it - TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); + TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo, list); } ++it; } - result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); + result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, list); } if (result) { int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); - result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo); - pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); - } - if (!result){ - undo(); + result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo, list); + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); + return true; + } } - return result; + undo(); + return false; } -bool TimelineFunctions::liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { - TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); + TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo, list); } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { - TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); + TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo, list); } } std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); for (const auto &clipId : clips) { - timeline->requestItemDeletion(clipId, undo, redo); + timeline->requestItemDeletion(clipId, undo, redo, list); } return true; } -bool TimelineFunctions::removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { - int res = timeline->requestClipsGroup(clips, undo, redo); + int res = timeline->requestClipsGroup(clips, undo, redo, list); if (res > -1) { - result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); + result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo, list); if (result) { - result = timeline->requestClipUngroup(clipId, undo, redo); + result = timeline->requestClipUngroup(clipId, undo, redo, list); } if (!result) { undo(); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); - result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo); + result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo, list); } } return result; } -bool TimelineFunctions::insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.x(), -1, true); bool result = true; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { - int res = timeline->requestClipsGroup(clips, undo, redo); + int res = timeline->requestClipsGroup(clips, undo, redo, list); if (res > -1) { - result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo); + result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo, list); if (result) { - result = timeline->requestClipUngroup(clipId, undo, redo); + result = timeline->requestClipUngroup(clipId, undo, redo, list); } else { pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); - result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo); + result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo, list); } } return result; } bool TimelineFunctions::requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); - res = copyClip(timeline, id, newId, state, undo, redo); + res = copyClip(timeline, id, newId, state, undo, redo, list); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { - res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo); + res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo, list); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); QScopedPointer transProps(timeline->m_allCompositions[id]->properties()); res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, - timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo); + timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo, list); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Sucessful copy, coping groups..."; - res = timeline->m_groups->copyGroups(mapping, undo, redo); + res = timeline->m_groups->copyGroups(mapping, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } + ModelUpdater::applyUpdates(undo, redo, list); return true; } void TimelineFunctions::showClipKeyframes(std::shared_ptr timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } bool TimelineFunctions::switchEnableState(std::shared_ptr timeline, int clipId) { PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); PlaylistState::ClipState state = PlaylistState::Disabled; bool disable = true; if (oldState == PlaylistState::Disabled) { bool audio = timeline->getTrackById(timeline->getClipTrackId(clipId))->isAudioTrack(); state = audio ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; disable = false; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool result = changeClipState(timeline, clipId, state, undo, redo); + Updates list; + bool result = changeClipState(timeline, clipId, state, undo, redo, list); if (result) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); } return result; } -bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) +bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo, + Updates &list) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; - bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); - UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); + Updates local_list; + bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo, local_list); + if (result) { + list.insert(list.end(), local_list.begin(), local_list.end()); + UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); + } return result; } bool TimelineFunctions::requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups pCore->clearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; - bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); + bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); - success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); + success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo, list); } - TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); - success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); + TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo, list); + success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo, list); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } bool TimelineFunctions::requestSplitVideo(std::shared_ptr timeline, int clipId, int videoTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups pCore->clearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); QList possibleTracks = QList() << videoTarget; if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage); return false; } int newId; - bool res = copyClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); + bool res = copyClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo, list); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); - success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); + success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo, list); } - TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); - success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); + TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo, list); + success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo, list); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } done = true; } if (done) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Split Video")); } return done; } void TimelineFunctions::setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack) { - std::function undo = []() { return true; }; - std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = compo->getForcedTrack() == -1; bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); - Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { + Fun redo = [timeline, cid, aTrack, autoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; - Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { + Fun undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; - if (local_redo()) { - PUSH_LAMBDA(local_undo, undo); - PUSH_LAMBDA(local_redo, redo); + Updates list; + list.emplace_back(new ChangeUpdate(cid, timeline, {TimelineModel::ItemATrack})); + bool result = redo(); + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } - pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp index 9ad9b75a8..0b4d3e933 100644 --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -1,94 +1,96 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle 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 TIMELINEFUNCTIONS_H #define TIMELINEFUNCTIONS_H #include "definitions.h" +#include "modelupdater.hpp" #include "undohelper.hpp" #include #include /** * @namespace TimelineFunction * @brief This namespace contains a list of static methods for advanced timeline editing features * based on timelinemodel methods */ class TimelineItemModel; struct TimelineFunctions { /* @brief Cuts a clip at given position If the clip is part of the group, all clips of the groups are cut at the same position. The group structure is then preserved for clips on both sides Returns true on success @param timeline : ptr to the timeline model @param clipId: Id of the clip to split @param position: position (in frames) where to cut */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position); /* This is the same function, except that it accumulates undo/redo */ - static bool requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo); + static bool requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo, Updates &list); /* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */ - static bool processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); + static bool processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo, Updates &list); /* @brief Makes a perfect copy of a given clip, but do not insert it */ - static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo, + Updates &list); /* @brief Request the addition of multiple clips to the timeline * If the addition of any of the clips fails, the entire operation is undone. * @returns true on success, false otherwise. * @param binIds the list of bin ids to be inserted * @param trackId the track where the insertion should happen * @param position the position at which the clips should be inserted * @param clipIds a return parameter with the ids assigned to the clips if success, empty otherwise */ static bool requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView); static int requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position); static bool requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition); static bool extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly); - static bool liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); - static bool removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); - static bool insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); + static bool liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list); + static bool removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list); + static bool insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, Updates &list); static bool insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite); static bool requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position); static void showClipKeyframes(std::shared_ptr timeline, int clipId, bool value); static void showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value); /* @brief If the clip is activated, disable, otherwise enable * @param timeline: pointer to the timeline that we modify * @param clipId: Id of the clip to modify * @param status: target status of the clip This function creates an undo object and returns true on success */ static bool switchEnableState(std::shared_ptr timeline, int clipId); /* @brief change the clip state and accumulates for undo/redo */ - static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); + static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo, Updates &list); static bool requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget); static bool requestSplitVideo(std::shared_ptr timeline, int clipId, int videoTarget); static void setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack); }; #endif diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index ffa6c9e6c..3d0c69d25 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,579 +1,572 @@ /*************************************************************************** * 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 "timelineitemmodel.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "bin/model/markerlistmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : TimelineModel(profile, undo_stack) { } void TimelineItemModel::finishConstruct(std::shared_ptr ptr, std::shared_ptr guideModel) { ptr->weak_this_ = ptr; ptr->m_groups = std::unique_ptr(new GroupsModel(ptr)); guideModel->registerSnapModel(ptr->m_snaps); } std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack) { std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack))); finishConstruct(ptr, std::move(guideModel)); return ptr; } TimelineItemModel::~TimelineItemModel() = default; QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const { READ_LOCK(); QModelIndex result; if (parent.isValid()) { auto trackId = int(parent.internalId()); Q_ASSERT(isTrack(trackId)); int clipId = getTrackById_const(trackId)->getClipByRow(row); if (clipId != -1) { result = createIndex(row, 0, quintptr(clipId)); } else { int compoId = getTrackById_const(trackId)->getCompositionByRow(row); if (compoId != -1) { result = createIndex(row, 0, quintptr(compoId)); } } } else if (row < getTracksCount() && row >= 0) { auto it = m_allTracks.cbegin(); std::advance(it, row); int trackId = (*it)->getId(); result = createIndex(row, column, quintptr(trackId)); } return result; } /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const { return index(clipIndex, 0, index(trackIndex)); }*/ QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = m_allClips.at(clipId)->getCurrentTrackId(); if (trackId == -1) { // Clip is not inserted in a track return QModelIndex(); } int row = getTrackById_const(trackId)->getRowfromClip(clipId); return index(row, 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); int trackId = m_allCompositions.at(compoId)->getCurrentTrackId(); return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const { // we retrieve iterator Q_ASSERT(m_iteratorTable.count(trackId) > 0); auto it = m_iteratorTable.at(trackId); int ind = (int)std::distance(m_allTracks.begin(), it); return index(ind); } QModelIndex TimelineItemModel::parent(const QModelIndex &index) const { READ_LOCK(); // qDebug() << "TimelineItemModel::parent"<< index; if (index == QModelIndex()) { return index; } const int id = static_cast(index.internalId()); if (!index.isValid() || isTrack(id)) { return QModelIndex(); } if (isClip(id)) { const int trackId = getClipTrackId(id); return makeTrackIndexFromID(trackId); } if (isComposition(id)) { const int trackId = getCompositionTrackId(id); return makeTrackIndexFromID(trackId); } return QModelIndex(); } int TimelineItemModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) { const int id = (int)parent.internalId(); if (!isTrack(id)) { // clips don't have children // if it is not a track, it is something invalid return 0; } return getTrackClipsCount(id) + getTrackCompositionsCount(id); } return getTracksCount(); } int TimelineItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QHash TimelineItemModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[ResourceRole] = "resource"; roles[ServiceRole] = "mlt_service"; roles[BinIdRole] = "binId"; roles[IsBlankRole] = "blank"; roles[StartRole] = "start"; roles[DurationRole] = "duration"; roles[MarkersRole] = "markers"; roles[KeyframesRole] = "keyframeModel"; roles[ShowKeyframesRole] = "showKeyframes"; roles[StatusRole] = "clipStatus"; roles[TypeRole] = "clipType"; roles[InPointRole] = "in"; roles[OutPointRole] = "out"; roles[FramerateRole] = "fps"; roles[GroupedRole] = "grouped"; roles[IsDisabledRole] = "disabled"; roles[IsAudioRole] = "audio"; roles[AudioLevelsRole] = "audioLevels"; roles[IsCompositeRole] = "composite"; roles[IsLockedRole] = "locked"; roles[FadeInRole] = "fadeIn"; roles[FadeOutRole] = "fadeOut"; roles[IsCompositionRole] = "isComposition"; roles[FileHashRole] = "hash"; roles[SpeedRole] = "speed"; roles[HeightRole] = "trackHeight"; roles[TrackTagRole] = "trackTag"; roles[ItemIdRole] = "item"; roles[ItemATrack] = "a_track"; roles[HasAudio] = "hasAudio"; roles[CanBeAudioRole] = "canBeAudio"; roles[CanBeVideoRole] = "canBeVideo"; roles[ReloadThumbRole] = "reloadThumb"; roles[ThumbsFormatRole] = "thumbsFormat"; roles[EffectNamesRole] = "effectNames"; roles[EffectsEnabledRole] = "isStackEnabled"; roles[GrabbedRole] = "isGrabbed"; return roles; } QVariant TimelineItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!m_tractor || !index.isValid()) { // qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id); // Get data for a clip switch (role) { // TODO case NameRole: case Qt::DisplayRole: { QString result = clip->getProperty("kdenlive:clipname"); if (result.isEmpty()) { result = clip->getProperty("resource"); if (!result.isEmpty()) { result = QFileInfo(result).fileName(); } else { result = clip->getProperty("mlt_service"); } } return result; } case ResourceRole: { QString result = clip->getProperty("resource"); if (result == QLatin1String("")) { result = clip->getProperty("mlt_service"); } return result; } case BinIdRole: return clip->binId(); case ServiceRole: return clip->getProperty("mlt_service"); break; case AudioLevelsRole: return clip->getAudioWaveform(); case HasAudio: return clip->audioEnabled(); case IsAudioRole: return clip->isAudioOnly(); case CanBeAudioRole: return clip->canBeAudio(); case CanBeVideoRole: return clip->canBeVideo(); case MarkersRole: { return QVariant::fromValue(clip->getMarkerModel().get()); } case KeyframesRole: { return QVariant::fromValue(clip->getKeyframeModel()); } case StatusRole: return QVariant::fromValue(clip->clipState()); case TypeRole: return QVariant::fromValue(clip->clipType()); case StartRole: return clip->getPosition(); case DurationRole: return clip->getPlaytime(); case GroupedRole: return (m_groups->isInGroup(id) && !isInSelection(id)); case EffectNamesRole: return clip->effectNames(); case InPointRole: return clip->getIn(); case OutPointRole: return clip->getOut(); case IsCompositionRole: return false; case ShowKeyframesRole: return clip->showKeyframes(); case FadeInRole: return clip->fadeIn(); case FadeOutRole: return clip->fadeOut(); case ReloadThumbRole: return clip->forceThumbReload; case SpeedRole: return clip->getSpeed(); case GrabbedRole: return clip->isGrabbed(); default: break; } } else if (isTrack(id)) { // qDebug() << "DATA REQUESTED FOR TRACK "<< id; switch (role) { case NameRole: case Qt::DisplayRole: { return getTrackById_const(id)->getProperty("kdenlive:track_name").toString(); } case DurationRole: // qDebug() << "DATA yielding duration" << m_tractor->get_playtime(); return getTrackById_const(id)->trackDuration(); case IsDisabledRole: // qDebug() << "DATA yielding mute" << 0; return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden(); case IsAudioRole: return getTrackById_const(id)->isAudioTrack(); case TrackTagRole: return getTrackTagById(id); case IsLockedRole: return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1; case HeightRole: { int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt(); - if ( collapsed > 0) { + if (collapsed > 0) { return collapsed; } int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt(); // qDebug() << "DATA yielding height" << height; return (height > 0 ? height : 60); } case ThumbsFormatRole: return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt(); case IsCompositeRole: { return Qt::Unchecked; } case EffectNamesRole: { return getTrackById_const(id)->effectNames(); } case EffectsEnabledRole: { return getTrackById_const(id)->stackEnabled(); } default: break; } } else if (isComposition(id)) { std::shared_ptr compo = m_allCompositions.at(id); switch (role) { case NameRole: case Qt::DisplayRole: case ResourceRole: case ServiceRole: return compo->displayName(); break; case IsBlankRole: // probably useless return false; case StartRole: return compo->getPosition(); case DurationRole: return compo->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case InPointRole: return 0; case OutPointRole: return 100; case BinIdRole: return 5; case KeyframesRole: { return QVariant::fromValue(compo->getEffectKeyframeModel()); } case ShowKeyframesRole: return compo->showKeyframes(); case ItemATrack: return compo->getForcedTrack(); case MarkersRole: { QVariantList markersList; return markersList; } case IsCompositionRole: return true; case GrabbedRole: return compo->isGrabbed(); default: break; } } else { qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role]; } return QVariant(); } void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value) { std::shared_ptr track = getTrackById(trackId); track->setProperty(name, value); QVector roles; if (name == QLatin1String("kdenlive:track_name")) { roles.push_back(NameRole); } else if (name == QLatin1String("kdenlive:locked_track")) { roles.push_back(IsLockedRole); } else if (name == QLatin1String("hide")) { roles.push_back(IsDisabledRole); - if(!track->isAudioTrack()) { + if (!track->isAudioTrack()) { pCore->requestMonitorRefresh(); } } else if (name == QLatin1String("kdenlive:thumbs_format")) { roles.push_back(ThumbsFormatRole); } if (!roles.isEmpty()) { QModelIndex ix = makeTrackIndexFromID(trackId); emit dataChanged(ix, ix, roles); } } void TimelineItemModel::setTrackStackEnabled(int tid, bool enable) { std::shared_ptr track = getTrackById(tid); track->setEffectStackEnabled(enable); QModelIndex ix = makeTrackIndexFromID(tid); emit dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole}); } void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr service) { std::shared_ptr track = getTrackById(tid); track->importEffects(service); } QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const { return getTrackById_const(tid)->getProperty(name); } int TimelineItemModel::getFirstVideoTrackIndex() const { int trackId; auto it = m_allTracks.cbegin(); while (it != m_allTracks.cend()) { trackId = (*it)->getId(); if (!(*it)->isAudioTrack()) { break; } ++it; } return trackId; } const QString TimelineItemModel::getTrackFullName(int tid) const { QString tag = getTrackTagById(tid); QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName; } const QString TimelineItemModel::groupsData() { return m_groups->toJson(); } bool TimelineItemModel::loadGroups(const QString &groupsData) { return m_groups->fromJson(groupsData); } bool TimelineItemModel::isInSelection(int cid) const { if (m_temporarySelectionGroup == -1 || !m_groups->isInGroup(cid)) { return false; } bool res = (m_groups->getRootId(cid) == m_temporarySelectionGroup); return res; } -void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) +void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) { - QVector roles; - if (start) { - roles.push_back(TimelineModel::StartRole); - if (updateThumb) { - roles.push_back(TimelineModel::InPointRole); - } - } - if (duration) { - roles.push_back(TimelineModel::DurationRole); - if (updateThumb) { - roles.push_back(TimelineModel::OutPointRole); - } - } emit dataChanged(topleft, bottomright, roles); } -void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) +void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) { - emit dataChanged(topleft, bottomright, roles); + emit dataChanged(topleft, bottomright, {role}); } void TimelineItemModel::buildTrackCompositing(bool rebuild) { auto it = m_allTracks.cbegin(); QScopedPointer field(m_tractor->field()); field->lock(); // Make sure all previous track compositing is removed if (rebuild) { QScopedPointer service(new Mlt::Service(field->get_service())); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } } QString composite = TransitionsRepository::get()->getCompositingTransition(); while (it != m_allTracks.cend()) { int trackId = getTrackMltIndex((*it)->getId()); if (!composite.isEmpty() && !(*it)->isAudioTrack()) { // video track, add composition Mlt::Transition *transition = TransitionsRepository::get()->getTransition(composite); transition->set("internal_added", 237); transition->set("always_active", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } else if ((*it)->isAudioTrack()) { // audio mix Mlt::Transition *transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); transition->set("internal_added", 237); transition->set("always_active", 1); transition->set("sum", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } ++it; } field->unlock(); if (composite.isEmpty()) { pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage); } } -void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) +void TimelineItemModel::_beginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destinationParent, + int destinationChild) { - emit dataChanged(topleft, bottomright, {role}); + beginMoveRows(sourceParent, sourceFirst, sourceLast, destinationParent, destinationChild); } void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k) { // qDebug()<<"FORWARDING beginRemoveRows"<. * ***************************************************************************/ #ifndef TIMELINEITEMMODEL_H #define TIMELINEITEMMODEL_H #include "timelinemodel.hpp" #include "undohelper.hpp" /* @brief This class is the thin wrapper around the TimelineModel that provides interface for the QML. It derives from AbstractItemModel to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been choosed because it is consistant with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class MarkerListModel; class TimelineItemModel : public TimelineModel { Q_OBJECT public: /* @brief construct a timeline object and returns a pointer to the created object @param undo_stack is a weak pointer to the undo stack of the project @param guideModel ptr to the guide model of the project */ static std::shared_ptr construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack); friend bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor); protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: ~TimelineItemModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; // QModelIndex makeIndex(int trackIndex, int clipIndex) const; /* @brief Creates an index based on the ID of the clip*/ QModelIndex makeClipIndexFromID(int clipId) const override; /* @brief Creates an index based on the ID of the compoition*/ QModelIndex makeCompositionIndexFromID(int compoId) const override; /* @brief Creates an index based on the ID of the track*/ QModelIndex makeTrackIndexFromID(int trackId) const override; QModelIndex parent(const QModelIndex &index) const override; Q_INVOKABLE void setTrackProperty(int tid, const QString &name, const QString &value); /* @brief Enabled/disabled a track's effect stack */ Q_INVOKABLE void setTrackStackEnabled(int tid, bool enable); Q_INVOKABLE QVariant getTrackProperty(int tid, const QString &name) const; /** @brief returns the lower video track index in timeline. **/ int getFirstVideoTrackIndex() const; const QString getTrackFullName(int tid) const; - void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) override; /** @brief Rebuild track compositing */ void buildTrackCompositing(bool rebuild = false); /** @brief Import track effects */ void importTrackEffects(int tid, std::weak_ptr service); const QString groupsData(); bool loadGroups(const QString &groupsData); /* @brief returns true if clip is in temporary selection group. */ bool isInSelection(int cid) const; - virtual void _beginRemoveRows(const QModelIndex &, int, int) override; virtual void _beginInsertRows(const QModelIndex &, int, int) override; - virtual void _endRemoveRows() override; + virtual void _beginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destinationParent, + int destinationChild) override; + virtual void _beginRemoveRows(const QModelIndex &, int, int) override; virtual void _endInsertRows() override; + virtual void _endMoveRows() override; + virtual void _endRemoveRows() override; virtual void _resetView() override; protected: // This is an helper function that finishes a constuction of a freshly created TimelineItemModel static void finishConstruct(std::shared_ptr ptr, std::shared_ptr guideModel); }; #endif diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index b2cc19cf9..fb81d00eb 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,2397 +1,2474 @@ /*************************************************************************** * 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 "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #ifdef LOGGING #include #include #endif #include "macros.hpp" int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_tractor(new Mlt::Tractor(*profile)) , m_snaps(new SnapModel()) , m_undoStack(undo_stack) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_temporarySelectionGroup(-1) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); m_blackClip->set("set.test_audio", 0); m_blackClip->set("length", INT_MAX); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); #ifdef LOGGING m_logFile = std::ofstream("log.txt"); m_logFile << "TEST_CASE(\"Regression\") {" << std::endl; m_logFile << "Mlt::Profile profile;" << std::endl; m_logFile << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl; m_logFile << "std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack);" << std::endl; m_logFile << "TimelineModel::next_id = 0;" << std::endl; m_logFile << "int dummy_id;" << std::endl; #endif } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.begin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getIn(); return pos; } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.end(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; --it; while (it != m_allTracks.begin()) { if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } --it; } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } -bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) +bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo, Updates &list) { qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView; if (trackId == -1) { return false; } Q_ASSERT(isClip(clipId)); std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; + Updates local_list; bool ok = true; int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { - ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo); + ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo, local_list); if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } - ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo); + ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo, local_list); if (!ok) { // qDebug()<<"-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------"; bool undone = local_undo(); Q_ASSERT(undone); return false; } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { #ifdef LOGGING m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; - bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); - if (res && logUndo) { - PUSH_UNDO(undo, redo, i18n("Move clip")); + Updates list; + bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo, list); + if (res) { + if (updateView) { + ModelUpdater::applyUpdates(undo, redo, list); + } + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Move clip")); + } } return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { #ifdef LOGGING m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); - res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); + res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, list, false); } else { - res = requestClipMove(clipId, trackId, position, false, false, undo, redo); + res = requestClipMove(clipId, trackId, position, false, false, undo, redo, list); } if (res) { undo(); } return res; } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int snapDistance, bool allowViewUpdate) { #ifdef LOGGING m_logFile << "timeline->suggestClipMove(" << clipId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); if (currentPos == position) { return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), ignored_pts, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible; if (allowViewUpdate) { possible = requestClipMove(clipId, trackId, position, false, false, false); } else { possible = requestClipMoveAttempt(clipId, trackId, position); } if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Easy int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { return false; } if (allowViewUpdate) { possible = requestClipMove(clipId, trackId, position, false, false, false); } else { possible = requestClipMoveAttempt(clipId, trackId, position); } return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getClipTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check after before the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value(); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); if (allowViewUpdate) { possible = requestClipMove(clipId, trackId, updatedPos, false, false, false); } else { possible = requestClipMoveAttempt(clipId, trackId, updatedPos); } if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int snapDistance) { #ifdef LOGGING m_logFile << "timeline->suggestCompositionMove(" << compoId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (currentPos == position || currentTrack != trackId) { return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool possible = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, false, undo, redo); + Updates list; + bool possible = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, false, undo, redo, list); qDebug() << "Original move success" << possible; if (possible) { bool undone = undo(); Q_ASSERT(undone); return position; } bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position; } -bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo, Updates &list) { qDebug() << "requestClipCreation " << binClipId; int clipId = TimelineModel::getNextId(); id = clipId; + Updates local_list; Fun local_undo = deregisterClip_lambda(clipId); QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } ClipModel::construct(shared_from_this(), bid, clipId, state); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { - res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); + res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo, local_list); } - res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); + res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo, local_list); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { #ifdef LOGGING m_logFile << "timeline->requestClipInsertion(" << binClipId.toStdString() << "," << trackId << " ," << position << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); - if (result && logUndo) { - PUSH_UNDO(undo, redo, i18n("Insert Clip")); + Updates list; + bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo, list); + if (result) { + if (refreshView) { + ModelUpdater::applyUpdates(undo, redo, list); + } + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Insert Clip")); + } } return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, - Fun &undo, Fun &redo) + Fun &undo, Fun &redo, Updates &list) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; + Updates local_list; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (type == ClipType::AV) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); - res = requestClipCreation(binClipId, id, audioDrop ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); - res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); + res = requestClipCreation(binClipId, id, audioDrop ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo, local_list); + res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo, local_list); if (m_videoTarget >= 0 && m_audioTarget == -1) { // No audio target defined, only extract video audioDrop = true; } else if (m_audioTarget >= 0) { if (getTrackById_const(m_audioTarget)->isLocked()) { // Audio target locked, only extract video audioDrop = true; } } if (res && (!audioDrop || !useTargets)) { int target_track = m_audioTarget; if (!useTargets) { target_track = getMirrorAudioTrackId(trackId); } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; + Updates audio_list; int newId; - res = requestClipCreation(binClipId, newId, PlaylistState::AudioOnly, audio_undo, audio_redo); + res = requestClipCreation(binClipId, newId, PlaylistState::AudioOnly, audio_undo, audio_redo, audio_list); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); - move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo); + move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo, audio_list); } // use lazy evaluation to group only if move was successful - res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); + res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, audio_list, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { + local_list.insert(local_list.end(), audio_list.begin(), audio_list.end()); UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); - res = requestClipCreation(binClipId, id, binClip->defaultState(), local_undo, local_redo); - res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); + res = requestClipCreation(binClipId, id, binClip->defaultState(), local_undo, local_redo, local_list); + res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo, local_list); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } -bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo) +bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(clipId)) { - return requestGroupDeletion(clipId, undo, redo); + return requestGroupDeletion(clipId, undo, redo, list); } - return requestClipDeletion(clipId, undo, redo); + return requestClipDeletion(clipId, undo, redo, list); } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestItemDeletion(" << itemId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, logUndo); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; bool res = false; QString actionLabel; if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); - res = requestClipDeletion(itemId, undo, redo); + res = requestClipDeletion(itemId, undo, redo, list); } else { actionLabel = i18n("Delete Composition"); - res = requestCompositionDeletion(itemId, undo, redo); + res = requestCompositionDeletion(itemId, undo, redo, list); } - if (res && logUndo) { - PUSH_UNDO(undo, redo, actionLabel); + if (res) { + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + PUSH_UNDO(undo, redo, actionLabel); + } } return res; } -bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) +bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo, Updates &list) { + Fun local_undo = []() { return true; }; + Fun local_redo = []() { return true; }; + Updates local_list; int trackId = getClipTrackId(clipId); if (trackId != -1) { - bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); + bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, local_list); if (!res) { - undo(); + local_undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip); return true; }; if (operation()) { emit removeFromSelection(clipId); - UPDATE_UNDO_REDO(operation, reverse, undo, redo); + list.insert(list.end(), local_list.begin(), local_list.end()); + UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); + UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } - undo(); + local_undo(); return false; } -bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) +bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo, Updates &list) { + Fun local_undo = []() { return true; }; + Fun local_redo = []() { return true; }; + Updates local_list; int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { - bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, undo, redo); + bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, local_undo, local_redo, local_list); if (!res) { - undo(); + local_undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { - emit removeFromSelection(compositionId); - UPDATE_UNDO_REDO(operation, reverse, undo, redo); + list.insert(list.end(), local_list.begin(), local_list.end()); + UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); + UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } - undo(); + local_undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { + updateView = true; + qDebug() << "Move group main clip" << clipId << "group" << groupId << "updateView" << updateView << "logUndo" << logUndo; std::function undo = []() { return true; }; std::function redo = []() { return true; }; - bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); - if (res && logUndo) { - PUSH_UNDO(undo, redo, i18n("Move group")); + Updates list; + bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, list); + if (res) { + if (updateView) { + ModelUpdater::applyUpdates(undo, redo, list); + } + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Move group")); + } } return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, - bool allowViewRefresh) + Updates &list, bool allowViewRefresh) { + qDebug() << "Move group inside clip" << clipId << "group" << groupId << "updateView" << updateView << "finalmove" << finalMove << "allowViewRefresh" + << allowViewRefresh; #ifdef LOGGING m_logFile << "timeline->requestGroupMove(" << clipId << "," << groupId << " ," << delta_track << ", " << delta_pos << ", " << (updateView ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; + Updates local_list; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) { int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : m_allCompositions[clipId1]->getPosition(); int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = (item == clipId) ? updateView : allowViewRefresh; if (isClip(item)) { - ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo); + ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, local_list); old_position[item] = m_allClips[item]->getPosition(); } else { - ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); + ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo, local_list); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; // if the topmost group is a AVSplit, then we will apply opposite movement to audio and video if (m_groups->getType(groupId) == GroupType::AVSplit) { if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { video_delta = -delta_track; } else { audio_delta = -delta_track; } } // Reverse sort. We need to insert from left to right to avoid confusing the view std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; bool updateThisView = (item == clipId) ? updateView : allowViewRefresh; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { - ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo); + ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo, local_list); } else { - ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); + ok = ok && + requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo, local_list); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestGroupDeletion(" << clipId << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool res = requestGroupDeletion(clipId, undo, redo); + Updates list; + bool res = requestGroupDeletion(clipId, undo, redo, list); if (res && logUndo) { - PUSH_UNDO(undo, redo, i18n("Remove group")); + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Remove group")); + } } return res; } -bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) +bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo, Updates &list) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); if (m_temporarySelectionGroup == current_group) { m_temporarySelectionGroup = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { - bool res = m_groups->ungroupItem(one_child, undo, redo); + bool res = m_groups->ungroupItem(one_child, undo, redo, list); if (!res) { undo(); return false; } } } for (int clip : all_items) { - bool res = requestClipDeletion(clip, undo, redo); + bool res = requestClipDeletion(clip, undo, redo, list); if (!res) { undo(); return false; } } for (int compo : all_compositions) { - bool res = requestCompositionDeletion(compo, undo, redo); + bool res = requestCompositionDeletion(compo, undo, redo, list); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { #ifdef LOGGING m_logFile << "timeline->requestItemResize(" << itemId << "," << size << " ," << (right ? "true" : "false") << ", " << (logUndo ? "true" : "false") << ", " << (snapDistance > 0 ? "true" : "false") << " ); " << std::endl; #endif if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (size <= 0) return -1; int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; + Updates list; int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); if (proposed_size >= 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { qDebug() << "+++MODEL REQUEST RESIZE (LOGUNDO) " << logUndo << ", SIZE: " << proposed_size; - success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); + success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, list, false); } else { - success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); + success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, list, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); auto items = m_groups->getLeaves(groupId); for (int id : items) { if (id == itemId) { all_items.insert(id); continue; } int start = getItemPosition(id); int end = in + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; for (int id : all_items) { - result = result && requestItemResize(id, size, right, logUndo, undo, redo); + result = result && requestItemResize(id, size, right, logUndo, undo, redo, list); } if (!result) { bool undone = undo(); Q_ASSERT(undone); return -1; } - if (result && logUndo) { - if (isClip(itemId)) { - PUSH_UNDO(undo, redo, i18n("Resize clip")); - } else { - PUSH_UNDO(undo, redo, i18n("Resize composition")); + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + if (isClip(itemId)) { + PUSH_UNDO(undo, redo, i18n("Resize clip")); + } else { + PUSH_UNDO(undo, redo, i18n("Resize composition")); + } } } return result ? size : -1; } -bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) +bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, Updates &list) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; - Fun update_model = [itemId, right, logUndo, this]() { - Q_ASSERT(isClip(itemId) || isComposition(itemId)); - if (getItemTrackId(itemId) != -1) { - QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); - notifyChange(modelIndex, modelIndex, !right, true, logUndo); - } - return true; - }; + Updates local_list; bool result = false; if (isClip(itemId)) { - result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); + result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, local_list, logUndo); } else { Q_ASSERT(isComposition(itemId)); - result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo); + result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, local_list); } if (result) { - if (!blockUndo) { - PUSH_LAMBDA(update_model, local_undo); - } - PUSH_LAMBDA(update_model, local_redo); - update_model(); + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; if (m_temporarySelectionGroup > -1) { m_groups->destructGroupItem(m_temporarySelectionGroup); // We don't log in undo the selection changes // int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin(); // requestClipUngroup(firstChild, undo, redo); m_temporarySelectionGroup = -1; } - int result = requestClipsGroup(ids, undo, redo, type); + int result = requestClipsGroup(ids, undo, redo, list, type); if (type == GroupType::Selection) { m_temporarySelectionGroup = result; } + if (result > -1) { + ModelUpdater::applyUpdates(undo, redo, list); + } if (result > -1 && logUndo && type != GroupType::Selection) { PUSH_UNDO(undo, redo, i18n("Group clips")); } return result; } -int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) +int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, Updates &list, GroupType type) { #ifdef LOGGING std::stringstream group; m_logFile << "{" << std::endl; m_logFile << "auto group = {"; bool deb = true; for (int clipId : ids) { if (deb) deb = false; else group << ", "; group << clipId; } m_logFile << group.str() << "};" << std::endl; m_logFile << "timeline->requestClipsGroup(group);" << std::endl; m_logFile << std::endl << "}" << std::endl; #endif QWriteLocker locker(&m_lock); for (int id : ids) { if (isClip(id)) { if (getClipTrackId(id) == -1) { return -1; } } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } - int groupId = m_groups->groupItems(ids, undo, redo, type); + int groupId = m_groups->groupItems(ids, undo, redo, list, type); if (type == GroupType::Selection && *(ids.begin()) == groupId) { // only one element selected, no group created return -1; } return groupId; } bool TimelineModel::requestClipUngroup(int id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestClipUngroup(" << id << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; bool result = true; if (id == m_temporarySelectionGroup) { // Ungrouping selection group, so get id of all children std::unordered_set leaves = m_groups->getDirectChildren(id); // Delete selection group without undo Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; - requestClipUngroup(id, tmp_undo, tmp_redo); + requestClipUngroup(id, tmp_undo, tmp_redo, list); m_temporarySelectionGroup = -1; // Parse children to find groups std::unordered_set groups; for (int item : leaves) { - if (m_groups->getLeaves(item).size() > 0) { - if (groups.count(item) <= 0) { - leaves.insert(item); - } + if (isGroup(item)) { + groups.insert(item); } } // destroy groups - for (int leave : leaves) { - result = requestClipUngroup(leave, undo, redo); + for (int group : groups) { + result = requestClipUngroup(group, undo, redo, list); if (!result) { break; } } } else { - result = requestClipUngroup(id, undo, redo); + result = requestClipUngroup(id, undo, redo, list); } - if (result && logUndo) { - PUSH_UNDO(undo, redo, i18n("Ungroup clips")); + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Ungroup clips")); + } } return result; } -bool TimelineModel::requestClipUngroup(int id, Fun &undo, Fun &redo) +bool TimelineModel::requestClipUngroup(int id, Fun &undo, Fun &redo, Updates &list) { - return m_groups->ungroupItem(id, undo, redo); + return m_groups->ungroupItem(id, undo, redo, list); } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { #ifdef LOGGING m_logFile << "timeline->requestTrackInsertion(" << position << ", dummy_id ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); + Updates list; + bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo, list); if (result) { + ModelUpdater::applyUpdates(undo, redo, list); PUSH_UNDO(undo, redo, i18n("Insert Track")); } return result; } -bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) +bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, Updates &list, + bool updateView) { + Q_UNUSED(list); // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, updateView); return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track #ifdef LOGGING m_logFile << "timeline->requestTrackDeletion(" << trackId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool result = requestTrackDeletion(trackId, undo, redo); + Updates list; + bool result = requestTrackDeletion(trackId, undo, redo, list); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } + ModelUpdater::applyUpdates(undo, redo, list); PUSH_UNDO(undo, redo, i18n("Delete Track")); } return result; } -bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) +bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo, Updates &list) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; + Updates local_list; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { - res = requestClipUngroup(clip, local_undo, local_redo); + res = requestClipUngroup(clip, local_undo, local_redo, local_list); } if (res) { - res = requestClipDeletion(clip, local_undo, local_redo); + res = requestClipDeletion(clip, local_undo, local_redo, local_list); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); return true; }; if (operation()) { + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView) { qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; if (reloadView) { // don't reload view on each track load on project opening _resetView(); } } void TimelineModel::registerClip(const std::shared_ptr &clip) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { QModelIndex root; _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return (*m_iteratorTable.at(trackId))->addEffect(effectId); } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->addEffect(effectId); } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); + Updates list; + bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo, list); if (res && initialDuration > 0) { + ModelUpdater::applyUpdates(undo, redo, list); PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } -bool TimelineModel::requestReset(Fun &undo, Fun &redo) +bool TimelineModel::requestReset(Fun &undo, Fun &redo, Updates &list) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { - ok = ok && requestTrackDeletion(trackId, undo, redo); + ok = ok && requestTrackDeletion(trackId, undo, redo, list); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::requestBestSnapPos(int pos, int length, const std::vector &pts, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::requestNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::requestPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionInsertion(\"composite\"," << trackId << " ," << position << "," << length << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, transProps, id, undo, redo); - if (result && logUndo) { - PUSH_UNDO(undo, redo, i18n("Insert Composition")); + Updates list; + bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, transProps, id, undo, redo, list); + if (result) { + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Insert Composition")); + } } return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, - Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo) + Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo, Updates &list) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, transProps); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; - bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, local_undo, local_redo); + Updates local_list; + bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, local_undo, local_redo, local_list); qDebug() << "trying to move" << trackId << "pos" << position << "succes " << res; if (res) { - res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); + res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, local_list); qDebug() << "trying to resize" << compositionId << "length" << length << "succes " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int compoId) const { return getTrackById_const(compoId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionMove(" << compoId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); - bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, undo, redo); + bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, undo, redo, list); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } - if (res && logUndo) { - PUSH_UNDO(undo, redo, i18n("Move composition")); - checkRefresh(min, max); + if (res) { + ModelUpdater::applyUpdates(undo, redo, list); + if (logUndo) { + PUSH_UNDO(undo, redo, i18n("Move composition")); + checkRefresh(min, max); + } } return res; } -bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo) +bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; + Updates local_list; bool ok = true; int old_trackId = getCompositionTrackId(compoId); if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); - return replantCompositions(compoId, updateView); + return replantCompositions(compoId); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); - ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, local_undo, local_redo); + ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, local_undo, local_redo, local_list); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } - ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, local_undo, local_redo); + ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, local_undo, local_redo, local_list); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, trackId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); - return replantCompositions(compoId, updateView); + return replantCompositions(compoId); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } + if (updateView) { + local_list.emplace_back(new ChangeUpdate(compoId, shared_from_this(), {TimelineModel::ItemATrack})); + } + list.insert(list.end(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } -bool TimelineModel::replantCompositions(int currentCompo, bool updateView) +bool TimelineModel::replantCompositions(int currentCompo) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.push_back({trackPos, compo.first}); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); - if (updateView) { - QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); - QVector roles; - roles.push_back(ItemATrack); - notifyChange(modelIndex, modelIndex, roles); - } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Constistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps"; return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { mlt_transition tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && getTrackMltIndex(m_allCompositions[compoId]->getATrack()) == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } Mlt::Producer *TimelineModel::producer() { auto *prod = new Mlt::Producer(tractor()); return prod; } void TimelineModel::checkRefresh(int start, int end) { int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; + Updates list; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); if (old_trackId != -1) { - getTrackById(old_trackId)->requestClipDeletion(clipId, false, true, local_undo, local_redo); + getTrackById(old_trackId)->requestClipDeletion(clipId, false, true, local_undo, local_redo, list); } m_allClips[clipId]->refreshProducerFromBin(); if (old_trackId != -1) { - getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); + getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, list); } + ModelUpdater::applyUpdates(local_undo, local_redo, list); } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } -bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) +bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; + Updates local_list; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { - success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); + success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, local_list); } if (success) { - success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); + success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo, local_list); } if (trackId != -1) { - success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); + success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, local_list); } if (!success) { local_undo(); return false; } + list.insert(list.begin(), local_list.begin(), local_list.end()); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { - result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); + result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo, list); } if (result) { - result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); + result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo, list); } else { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); return false; } } else { // If clip is not inserted on a track, we just change the producer - m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); + m_allClips[clipId]->useTimewarpProducer(speed, undo, redo, list); } if (result) { + ModelUpdater::applyUpdates(undo, redo, list); PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.begin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.begin() ? trackId : (*it)->getId(); } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.end()) { ++it; if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.end() ? trackId : (*it)->getId(); } diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index b9b7d976d..6b42e30d3 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,727 +1,730 @@ /*************************************************************************** * 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 TIMELINEMODEL_H #define TIMELINEMODEL_H #include "definitions.h" +#include "modelupdater.hpp" #include "undohelper.hpp" #include #include #include #include #include #include #include #include //#define LOGGING 1 // If set to 1, we log the actions requested to the timeline as a reproducer script #ifdef LOGGING #include #endif class AssetParameterModel; class EffectStackModel; class ClipModel; class CompositionModel; class DocUndoStack; class GroupsModel; class SnapModel; class TimelineItemModel; class TrackModel; /* @brief This class represents a Timeline object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications. This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in any interactions with the objects and have nothing to do with Melt IDs. This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the version in timeline. This is also required to generate the Undo/Redo operators The undo/redo system is designed around lambda functions. Each time a function executes an elementary change to the model, it writes the corresponding operation and its reverse, respectively in the redo and the undo lambdas. This way, if an operation fails for some reason, we can easily cancel the steps that have been done so far without corrupting anything. The other advantage is that operations are easy to compose, and you get a undo/redo pair for free no matter in which way you combine them. Most of the modification functions are named requestObjectAction. Eg, if the object is a clip and we want to move it, we call requestClipMove. These functions always return a bool indicating success, and when they return false they should guarantee than nothing has been modified. Most of the time, these functions come in two versions: the first one is the entry point if you want to perform only the action (and not compose it with other actions). This version will generally automatically push and Undo object on the Application stack, in case the user later wants to cancel the operation. It also generally goes the extra mile to ensure the operation is done in a way that match the user's expectation: for example requestClipMove checks whether the clip belongs to a group and in that case actually mouves the full group. The other version of the function, if it exists, is intended for composition (using the action as part of a complex operation). It takes as input the undo/redo lambda corresponding to the action that is being performed and accumulates on them. Note that this version does the minimal job: in the example of the requestClipMove, it will not move the full group if the clip is in a group. Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it. We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far. For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good, otherwise we revert what we've already done. This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost. It derives from AbstractItemModel (indirectly through TimelineItemModel) to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been choosed because it is consistant with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class TimelineModel : public QAbstractItemModel_shared_from_this { Q_OBJECT protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: friend class TrackModel; template friend class MoveableItem; friend class ClipModel; friend class CompositionModel; friend class GroupsModel; friend class TimelineController; friend struct TimelineFunctions; + friend class ModelUpdater; /// Two level model: tracks and clips on track enum { NameRole = Qt::UserRole + 1, ResourceRole, /// clip only ServiceRole, /// clip only IsBlankRole, /// clip only StartRole, /// clip only BinIdRole, /// clip only MarkersRole, /// clip only StatusRole, /// clip only TypeRole, /// clip only KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only CanBeAudioRole, /// clip only CanBeVideoRole, /// clip only IsDisabledRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only TrackTagRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only IsCompositionRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only ItemATrack, /// composition only ItemIdRole, ThumbsFormatRole, /// track only EffectNamesRole, // track and clip only EffectsEnabledRole,// track and clip only GrabbedRole /// clip+composition only }; virtual ~TimelineModel(); Mlt::Tractor *tractor() const { return m_tractor.get(); } /* @brief Load tracks from the current tractor, used on project opening */ void loadTractor(); /* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc */ Mlt::Producer *producer(); Mlt::Profile *getProfile(); /* @brief returns the number of tracks */ int getTracksCount() const; /* @brief returns the track index (id) from its position */ int getTrackIndexFromPosition(int pos) const; /* @brief returns the number of clips */ int getClipsCount() const; /* @brief returns the number of compositions */ int getCompositionsCount() const; /* @brief Returns the id of the track containing clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipTrackId(int clipId) const; /* @brief Returns the id of the track containing composition (-1 if it is not inserted) @param clipId Id of the composition to test */ Q_INVOKABLE int getCompositionTrackId(int compoId) const; /* @brief Convenience function that calls either of the previous ones based on item type*/ int getItemTrackId(int itemId) const; Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* Returns the current speed of a clip */ double getClipSpeed(int clipId) const; /* @brief Helper function to query the amount of free space around a clip * @param clipId: the queried clip. If it is not inserted on a track, this functions returns 0 * @param after: if true, we return the blank after the clip, otherwise, before. */ int getBlankSizeNearClip(int clipId, bool after) const; /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getClipSplitPartner(int clipId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isTrack(int id) const; - /* @brief Helper function that returns true if the given ID corresponds to a track */ + /* @brief Helper function that returns true if the given ID corresponds to a group */ bool isGroup(int id) const; /* @brief Given a composition Id, returns its underlying parameter model */ std::shared_ptr getCompositionParameterModel(int compoId) const; /* @brief Given a clip Id, returns its underlying effect stack model */ std::shared_ptr getClipEffectStackModel(int clipId) const; /* @brief Returns the position of clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipPosition(int clipId) const; Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration); /* @brief Returns the closest snap point within snapDistance */ Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance); /** @brief Return the previous track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getPreviousTrackId(int trackId); /** @brief Return the next track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getNextTrackId(int trackId); /* @brief Returns the in cut position of a clip @param clipId Id of the clip to test */ int getClipIn(int clipId) const; /* @brief Returns the bin id of the clip master @param clipId Id of the clip to test */ const QString getClipBinId(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ int getClipPlaytime(int clipId) const; /* @brief Returns the size of the clip's frame (widthxheight) @param clipId Id of the clip to test */ QSize getClipFrameSize(int clipId) const; /* @brief Returns the number of clips in a given track @param trackId Id of the track to test */ int getTrackClipsCount(int trackId) const; /* @brief Returns the number of compositions in a given track @param trackId Id of the track to test */ int getTrackCompositionsCount(int trackId) const; /* @brief Returns the position of the track in the order of the tracks @param trackId Id of the track to test */ int getTrackPosition(int trackId) const; /* @brief Returns the track's index in terms of mlt's internal representation */ int getTrackMltIndex(int trackId) const; /* @brief Returns a sort position for tracks. * @param separated: if true, the tracks will be sorted like: V2,V1,A1,A2 * Otherwise, the tracks will be sorted like V2,A2,V1,A1 */ int getTrackSortValue(int trackId, bool separated) const; /* @brief Returns the ids of the tracks below the given track in the order of the tracks Returns an empty list if no track available @param trackId Id of the track to test */ QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const; /* @brief Returns the MLT track index of the video track just below the given trackC @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Retuns the Id of the corresponding audio track. If trackId corresponds to video1, this will return audio 1 and so on */ int getMirrorAudioTrackId(int trackId) const; /* @brief Move a clip to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param clipId is the ID of the clip @param trackId is the ID of the target track @param position is the position where we want to move @param updateView if set to false, no signal is sent to qml @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false); /* @brief Move a composition to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param transid is the ID of the composition @param trackId is the ID of the track */ Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true); /* Same function, but accumulates undo and redo, and doesn't check for group*/ - bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); - bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo); + bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo, Updates &list); + bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo, Updates &list); /* @brief Given an intended move, try to suggest a more valid one (accounting for snaps and missing UI calls) @param clipId id of the clip to move @param trackId id of the target track @param position target position @param snapDistance the maximum distance for a snap result, -1 for no snapping of the clip @param dontRefreshMasterClip when false, no view refresh is attempted */ Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int snapDistance = -1, bool allowViewUpdate = true); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int snapDistance = -1); /* @brief Request clip insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param binClipId id of the clip in the bin @param track Id of the track where to insert @param position Requested position @param ID return parameter of the id of the inserted clip @param logUndo if set to false, no undo object is stored @param refreshView whether the view should be refreshed @param useTargets: if true, the Audio/video split will occur on the set targets. Otherwise, they will be computed as an offset from the middle line */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false, bool useTargets = true); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, - Fun &redo); + Fun &redo, Updates &list); /* @brief Creates a new clip instance without inserting it. This action is undoable, returns true on success @param binClipId: Bin id of the clip to insert @param id: return parameter for the id of the newly created clip. @param state: The desired clip state (original, audio/video only). */ - bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo); + bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo, Updates &list); /* @brief Deletes the given clip or composition from the timeline This action is undoable Returns true on success. If it fails, nothing is modified. If the clip/composition is in a group, the call is deferred to requestGroupDeletion @param clipId is the ID of the clip/composition @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ - bool requestItemDeletion(int clipId, Fun &undo, Fun &redo); + bool requestItemDeletion(int clipId, Fun &undo, Fun &redo, Updates &list); /* @brief Move a group to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clips in the group are not in inserted in a track yet, they get inserted for the first time. @param clipId is the id of the clip that triggers the group move @param groupId is the id of the group @param delta_track is the delta applied to the track index @param delta_pos is the requested position change @param updateView if set to false, no signal is sent to qml for the clip clipId @param logUndo if set to true, an undo object is created @param allowViewRefresh if false, the view will never get updated (useful for suggestMove) */ bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); - bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, + bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, Updates &list, bool allowViewRefresh = true); /* @brief Deletes all clips inside the group that contains the given clip. This action is undoable Note that if their is a hierarchy of groups, all of them will be deleted. Returns true on success. If it fails, nothing is modified. @param clipId is the id of the clip that triggers the group deletion */ Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true); - bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo); + bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo, Updates &list); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns the real size reached (can be different, if snapping occurs). If it fails, nothing is modified, and -1 is returned @param itemId is the ID of the item @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param logUndo if set to true, an undo object is created @param snap if set to true, the resize order will be coerced to use the snapping grid */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* Same function, but accumulates undo and redo and doesn't deal with snapping*/ - bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false); + bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, Updates &list); /* @brief Group together a set of ids The ids are either a group ids or clip ids. The involved clip must already be inserted in a track This action is undoable Returns the group id on success, -1 if it fails and nothing is modified. Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well. @param ids Set of ids to group */ int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal); - int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal); + int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, Updates &list, GroupType type = GroupType::Normal); /* @brief Destruct the topmost group containing clip This action is undoable Returns true on success. If it fails, nothing is modified. @param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well) */ bool requestClipUngroup(int id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ - bool requestClipUngroup(int id, Fun &undo, Fun &redo); + bool requestClipUngroup(int id, Fun &undo, Fun &redo, Updates &list); /* @brief Create a track at given position This action is undoable Returns true on success. If it fails, nothing is modified. @param Requested position (order). If set to -1, the track is inserted last. @param id is a return parameter that holds the id of the resulting track (-1 on failure) */ bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false); /* Same function, but accumulates undo and redo*/ - bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true); + bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, Updates &list, bool updateView = true); /* @brief Delete track with given id This also deletes all the clips contained in the track. This action is undoable Returns true on success. If it fails, nothing is modified. @param trackId id of the track to delete */ bool requestTrackDeletion(int trackId); /* Same function, but accumulates undo and redo*/ - bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo); + bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo, Updates &list); /* @brief Get project duration Returns the duration in frames */ int duration() const; static int seekDuration; // Duration after project end where seeking is allowed /* @brief Get all the elements of the same group as the given clip. If there is a group hierarchy, only the topmost group is considered. @param clipId id of the clip to test */ std::unordered_set getGroupElements(int clipId); /* @brief Removes all the elements on the timeline (tracks and clips) */ - bool requestReset(Fun &undo, Fun &redo); + bool requestReset(Fun &undo, Fun &redo, Updates &list); /* @brief Updates the current the pointer to the current undo_stack Must be called for example when the doc change */ void setUndoStack(std::weak_ptr undo_stack); /* @brief Requests the best snapped position for a clip @param pos is the clip's requested position @param length is the clip's duration @param pts snap points to ignore (for example currently moved clip) @param snapDistance the maximum distance for a snap result, -1 for no snapping @returns best snap position or -1 if no snap point is near */ int requestBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int snapDistance = -1); /* @brief Requests the next snapped point @param pos is the current position */ int requestNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int requestPreviousSnapPos(int pos); /* @brief Add a new snap point @param pos is the current position */ void addSnap(int pos); /* @brief Remove snap point @param pos is the current position */ void removeSnap(int pos); /* @brief Request composition insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param transitionId Identifier of the Mlt transition to insert (as given by repository) @param track Id of the track where to insert @param position Requested position @param length Requested initial length. @param id return parameter of the id of the inserted composition @param logUndo if set to false, no undo object is stored */ bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, Mlt::Properties *transProps, - int &id, Fun &undo, Fun &redo); + int &id, Fun &undo, Fun &redo, Updates &list); /* @brief This function change the global (timeline-wise) enabled state of the effects It disables/enables track and clip effects (recursively) */ void setTimelineEffectsEnabled(bool enabled); /* @brief Get a timeline clip id by its position or -1 if not found */ int getClipByPosition(int trackId, int position) const; /* @brief Get a timeline composition id by its starting position or -1 if not found */ int getCompositionByPosition(int trackId, int position) const; /* @brief Returns a list of all items that are intersect with a given range. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param start is the position where we the items should start * @param end is the position after which items will not be selected, set to -1 to get all clips on track * @param listCompositions if enabled, the list will also contains composition ids */ std::unordered_set getItemsInRange(int trackId, int start, int end = -1, bool listCompositions = true); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Inform asset view of duration change */ virtual void adjustAssetRange(int clipId, int in, int out); void requestClipReload(int clipId); void requestClipUpdate(int clipId, const QVector &roles); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); std::shared_ptr getTrackEffectStackModel(int trackId); /** @brief Add slowmotion effect to clip in timeline. @param clipId id of the target clip @param speed: speed in percentage. 100 corresponds to original speed, 50 to half the speed This functions create an undo object and also apply the effect to the corresponding audio if there is any. Returns true on success, false otherwise (and nothing is modifed) */ bool requestClipTimeWarp(int clipId, double speed); /* @brief Same function as above, but doesn't check for paired audio and accumulate undo/redo */ - bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo); + bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo, Updates &list); void replugClip(int clipId); /** @brief Refresh the tractor profile in case a change was requested. */ void updateProfile(Mlt::Profile *profile); protected: /* @brief Register a new track. This is a call-back meant to be called from TrackModel @param pos indicates the number of the track we are adding. If this is -1, then we add at the end. */ void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip); /* @brief Register a new composition. This is a call-back meant to be called from CompositionModel */ void registerComposition(const std::shared_ptr &composition); /* @brief Register a new group. This is a call-back meant to be called from GroupsModel */ void registerGroup(int groupId); /* @brief Deregister and destruct the track with given id. @parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor */ Fun deregisterTrack_lambda(int id, bool updateView = false); /* @brief Return a lambda that deregisters and destructs the clip with given id. Note that the clip must already be deleted from its track and groups. */ Fun deregisterClip_lambda(int id); /* @brief Return a lambda that deregisters and destructs the composition with given id. */ Fun deregisterComposition_lambda(int compoId); /* @brief Deregister a group with given id */ void deregisterGroup(int id); /* @brief Helper function to get a pointer to the track, given its id */ std::shared_ptr getTrackById(int trackId); const std::shared_ptr getTrackById_const(int trackId) const; /*@brief Helper function to get a pointer to a clip, given its id*/ std::shared_ptr getClipPtr(int clipId) const; /*@brief Helper function to get a pointer to a composition, given its id*/ std::shared_ptr getCompositionPtr(int compoId) const; /* @brief Returns next valid unique id to create an object */ static int getNextId(); /* @brief unplant and the replant all the compositions in the correct order @param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1 */ - bool replantCompositions(int currentCompo, bool updateView); + bool replantCompositions(int currentCompo); /* @brief Unplant the composition with given Id */ bool unplantComposition(int compoId); /* Same function but accumulates undo and redo, and doesn't check for group*/ - bool requestClipDeletion(int clipId, Fun &undo, Fun &redo); - bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo); + bool requestClipDeletion(int clipId, Fun &undo, Fun &redo, Updates &list); + bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo, Updates &list); /** @brief Check tracks duration and update black track accordingly */ void updateDuration(); /** @brief Get a track tag (A1, V1, V2,...) through its id */ const QString getTrackTagById(int trackId) const; /** @brief Attempt to make a clip move without ever updating the view */ bool requestClipMoveAttempt(int clipId, int trackId, int position); public: /* @brief Debugging function that checks consistency with Mlt objects */ bool checkConsistency(); protected: /* @brief Refresh project monitor if cursor was inside range */ void checkRefresh(int start, int end); /* @brief Send signal to require clearing effet/composition view */ void clearAssetView(int itemId); signals: /* @brief signal triggered by clearAssetView */ void requestClearAssetView(int); void requestMonitorRefresh(); /* @brief signal triggered by track operations */ void invalidateZone(int in, int out); /* @brief signal triggered when a track duration changed (insertion/deletion) */ void durationUpdated(); /* @brief an item was deleted, make sure it is removed from selection */ void removeFromSelection(int id); protected: std::unique_ptr m_tractor; std::list> m_allTracks; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id. std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers std::unordered_map> m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers static int next_id; // next valid id to assign std::unique_ptr m_groups; std::shared_ptr m_snaps; std::unordered_set m_allGroups; // ids of all the groups std::weak_ptr m_undoStack; Mlt::Profile *m_profile; // The black track producer. Its length / out should always be adjusted to the projects's length std::unique_ptr m_blackClip; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access #ifdef LOGGING std::ofstream m_logFile; // this is a temporary debug member to help reproduce issues #endif bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself // id of the currently selected group in timeline, should be destroyed on each new selection int m_temporarySelectionGroup; // The index of the temporary overlay track in tractor, or -1 if not connected int m_overlayTrackCount; // The preferred audio target for clip insertion or -1 if not defined int m_audioTarget; // The preferred video target for clip insertion or -1 if not defined int m_videoTarget; // what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel protected: - virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _beginInsertRows(const QModelIndex &, int, int) = 0; - virtual void _endRemoveRows() = 0; + virtual void _beginMoveRows(const QModelIndex &, int, int, const QModelIndex &, int) = 0; + virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _endInsertRows() = 0; - virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0; + virtual void _endMoveRows() = 0; + virtual void _endRemoveRows() = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif diff --git a/src/timeline2/model/trackmodel.cpp b/src/timeline2/model/trackmodel.cpp index ef5b49d14..5b200536d 100644 --- a/src/timeline2/model/trackmodel.cpp +++ b/src/timeline2/model/trackmodel.cpp @@ -1,1067 +1,1103 @@ /*************************************************************************** * 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 "trackmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "snapmodel.hpp" #include "timelinemodel.hpp" #include #include #include #include TrackModel::TrackModel(const std::weak_ptr &parent, int id, const QString &trackName, bool audioTrack) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) , m_lock(QReadWriteLock::Recursive) { if (auto ptr = parent.lock()) { m_track = std::shared_ptr(new Mlt::Tractor(*ptr->getProfile())); m_playlists[0].set_profile(*ptr->getProfile()); m_playlists[1].set_profile(*ptr->getProfile()); m_track->insert_track(m_playlists[0], 0); m_track->insert_track(m_playlists[1], 1); if (!trackName.isEmpty()) { m_track->set("kdenlive:track_name", trackName.toUtf8().constData()); } if (audioTrack) { m_track->set("kdenlive:audio_track", 1); for (int i = 0; i < 2; i++) { m_playlists[i].set("hide", 1); } } m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight()); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](){ if (auto ptr2 = m_parent.lock()) { QModelIndex ix = ptr2->makeTrackIndexFromID(m_id); ptr2->dataChanged(ix, ix, {TimelineModel::EffectNamesRole}); } }); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) { if (auto ptr = parent.lock()) { m_track = std::shared_ptr(new Mlt::Tractor(mltTrack)); m_playlists[0] = *m_track->track(0); m_playlists[1] = *m_track->track(1); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::~TrackModel() { m_track->remove_track(1); m_track->remove_track(0); } int TrackModel::construct(const std::weak_ptr &parent, int id, int pos, const QString &trackName, bool audioTrack) { std::shared_ptr track(new TrackModel(parent, id, trackName, audioTrack)); id = track->m_id; if (auto ptr = parent.lock()) { ptr->registerTrack(std::move(track), pos); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } return id; } int TrackModel::getClipsCount() { READ_LOCK(); #ifdef QT_DEBUG int count = 0; for (int j = 0; j < 2; j++) { for (int i = 0; i < m_playlists[j].count(); i++) { if (!m_playlists[j].is_blank(i)) { count++; } } } Q_ASSERT(count == static_cast(m_allClips.size())); #else int count = m_allClips.size(); #endif return count; } Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // By default, insertion occurs in topmost track // Find out the clip id at position int target_clip = m_playlists[0].get_clip_index_at(position); int count = m_playlists[0].count(); // we create the function that has to be executed after the melt order. This is essentially book-keeping auto end_function = [clipId, this, position, updateView, finalMove]() { if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_allClips[clip->getId()] = clip; // store clip // update clip position and track clip->setPosition(position); clip->setCurrentTrackId(getId()); int new_in = clip->getPosition(); int new_out = new_in + clip->getPlaytime(); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (updateView) { - int clip_index = getRowfromClip(clipId); - ptr->_beginInsertRows(ptr->makeTrackIndexFromID(getId()), clip_index, clip_index); - ptr->_endInsertRows(); bool audioOnly = clip->isAudioOnly(); if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(new_in, new_out); } if (!audioOnly && finalMove && !isAudioTrack()) { ptr->invalidateZone(new_in, new_out); } } return true; } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; if (target_clip >= count && isBlankAt(position)) { // In that case, we append after, in the first playlist return [this, position, clipId, end_function, finalMove]() { if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); if (finalMove) { ptr->updateDuration(); } return index != -1 && end_function(); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } if (isBlankAt(position)) { int blank_end = getBlankEnd(position); int length = -1; if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); length = clip->getPlaytime(); } if (blank_end >= position + length) { return [this, position, clipId, end_function]() { if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); return index != -1 && end_function(); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } } return []() { return false; }; } -bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) +bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } if (auto ptr = m_parent.lock()) { if (isAudioTrack() && !ptr->getClipPtr(clipId)->canBeAudio()) { return false; } if (!isAudioTrack() && !ptr->getClipPtr(clipId)->canBeVideo()) { return false; } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool res = true; if (ptr->getClipPtr(clipId)->clipState() != PlaylistState::Disabled) { - res = res && ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); + res = res && + ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo, list); } auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove); res = res && operation(); if (res) { + if (updateView) { + list.emplace_back(std::make_shared(clipId, ptr, getId(), position, true)); + } auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool undone = local_undo(); Q_ASSERT(undone); return false; } return false; } void TrackModel::replugClip(int clipId) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); std::unique_ptr prod(m_playlists[target_track].replace_with_blank(target_clip)); if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_playlists[target_track].insert_at(clip_position, *clip, 1); if (!clip->isAudioOnly() && !isAudioTrack()) { ptr->invalidateZone(clip->getIn(), clip->getOut()); } if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(clip->getIn(), clip->getOut()); } } m_playlists[target_track].consolidate_blanks(); m_playlists[target_track].unlock(); } Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allClips[clipId]->getPosition(); bool audioOnly = m_allClips[clipId]->isAudioOnly(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, this]() { auto clip_loc = getClipIndexAt(clip_position); - if (updateView) { - int old_clip_index = getRowfromClip(clipId); - auto ptr = m_parent.lock(); - ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); - ptr->_endRemoveRows(); + auto ptr = m_parent.lock(); + if (updateView && ptr) { + if (!audioOnly && !isHidden() && !isAudioTrack()) { + // only refresh monitor if not an audio track and not hidden + ptr->checkRefresh(old_in, old_out); + } + if (!audioOnly && finalMove && !isAudioTrack()) { + ptr->invalidateZone(old_in, old_out); + } } int target_track = clip_loc.first; int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); auto prod = m_playlists[target_track].replace_with_blank(target_clip); if (prod != nullptr) { m_playlists[target_track].consolidate_blanks(); m_allClips[clipId]->setCurrentTrackId(-1); m_allClips.erase(clipId); delete prod; m_playlists[target_track].unlock(); - if (auto ptr = m_parent.lock()) { + if (ptr) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); if (finalMove) { if (!audioOnly && !isAudioTrack()) { ptr->invalidateZone(old_in, old_out); } if (target_clip >= m_playlists[target_track].count()) { // deleted last clip in playlist ptr->updateDuration(); } } if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(old_in, old_out); } } return true; } m_playlists[target_track].unlock(); return false; }; } -bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo) +bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (isLocked()) { return false; } auto old_clip = m_allClips[clipId]; int old_position = old_clip->getPosition(); qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView; auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove); if (operation()) { + if (updateView) { + list.emplace_back(std::make_shared(clipId, m_parent, getId(), old_position, true)); + } auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } int TrackModel::getBlankSizeAtPos(int frame) { READ_LOCK(); int min_length = 0; for (int i = 0; i < 2; ++i) { int ix = m_playlists[i].get_clip_index_at(frame); if (m_playlists[i].is_blank(ix)) { int blank_length = m_playlists[i].clip_length(ix); if (min_length == 0 || (blank_length > 0 && blank_length < min_length)) { min_length = blank_length; } } } return min_length; } int TrackModel::getBlankSizeNearClip(int clipId, bool after) { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int track = clip_loc.first; int index = clip_loc.second; int other_index; // index in the other track int other_track = (track + 1) % 2; if (after) { int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); other_index = m_playlists[other_track].get_clip_index_at(first_pos); index++; } else { int last_pos = m_playlists[track].clip_start(index) - 1; other_index = m_playlists[other_track].get_clip_index_at(last_pos); index--; } if (index < 0) return 0; int length = INT_MAX; if (index < m_playlists[track].count()) { if (!m_playlists[track].is_blank(index)) { return 0; } length = std::min(length, m_playlists[track].clip_length(index)); } if (other_index < m_playlists[other_track].count()) { if (!m_playlists[other_track].is_blank(other_index)) { return 0; } length = std::min(length, m_playlists[other_track].clip_length(other_index)); } return length; } int TrackModel::getBlankSizeNearComposition(int compoId, bool after) { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); int clip_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(clip_position) > 0); Q_ASSERT(m_compoPos[clip_position] == compoId); auto it = m_compoPos.find(clip_position); int clip_length = m_allCompositions[compoId]->getPlaytime(); int length = INT_MAX; if (after) { ++it; if (it != m_compoPos.end()) { return it->first - clip_position - clip_length; } } else { if (it != m_compoPos.begin()) { --it; return clip_position - it->first - m_allCompositions[it->second]->getPlaytime(); } return clip_position; } return length; } Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; Q_ASSERT(target_clip < m_playlists[target_track].count()); int size = out - in + 1; bool checkRefresh = false; if (!isHidden() && !isAudioTrack()) { checkRefresh = true; } auto update_snaps = [clipId, old_in, old_out, checkRefresh, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (checkRefresh) { ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); // ptr->adjustAssetRange(clipId, m_allClips[clipId]->getIn(), m_allClips[clipId]->getOut()); } } else { qDebug() << "Error : clip resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; int delta = m_allClips[clipId]->getPlaytime() - size; if (delta == 0) { return []() { return true; }; } // qDebug() << "RESIZING CLIP: " << clipId << " FROM: " << delta; if (delta > 0) { // we shrink clip return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() { int target_clip_mutable = target_clip; int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable; // insert blank to space that is going to be empty // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1 m_playlists[target_track].insert_blank(blank_index, delta - 1); if (!right) { m_allClips[clipId]->setPosition(clip_position + delta); // Because we inserted blank before, the index of our clip has increased target_clip_mutable++; } int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); // make sure to do this after, to avoid messing the indexes m_playlists[target_track].consolidate_blanks(); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } } return err == 0; }; } int blank = -1; int other_blank_end = getBlankEnd(clip_position, (target_track + 1) % 2); if (right) { if (target_clip == m_playlists[target_track].count() - 1 && other_blank_end >= out) { // clip is last, it can always be extended return [this, target_clip, target_track, in, out, update_snaps, clipId]() { // color, image and title clips can have unlimited resize QScopedPointer clip(m_playlists[target_track].get_clip(target_clip)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } int err = m_playlists[target_track].resize_clip(target_clip, in, out); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); if (m_playlists[target_track].count() - 1 == target_clip) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } return err == 0; }; } blank = target_clip + 1; } else { if (target_clip == 0) { // clip is first, it can never be extended on the left return []() { return false; }; } blank = target_clip - 1; } if (m_playlists[target_track].is_blank(blank)) { int blank_length = m_playlists[target_track].clip_length(blank); if (blank_length + delta >= 0 && other_blank_end >= out) { return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() { int target_clip_mutable = target_clip; int err = 0; if (blank_length + delta == 0) { err = m_playlists[target_track].remove(blank); if (!right) { target_clip_mutable--; } } else { err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1); } if (err == 0) { QScopedPointer clip(m_playlists[target_track].get_clip(target_clip_mutable)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); } if (!right && err == 0) { m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable)); } if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); return err == 0; }; } } return []() { return false; }; } int TrackModel::getId() const { return m_id; } int TrackModel::getClipByPosition(int position) { READ_LOCK(); QSharedPointer prod(nullptr); if (m_playlists[0].count() > 0) { prod = QSharedPointer(m_playlists[0].get_clip_at(position)); } if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { prod = QSharedPointer(m_playlists[1].get_clip_at(position)); } if (!prod || prod->is_blank()) { return -1; } return prod->get_int("_kdenlive_cid"); } int TrackModel::getCompositionByPosition(int position) { READ_LOCK(); for (const auto &comp : m_compoPos) { if (comp.first == position) { return comp.second; } else if (comp.first < position) { if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) { return comp.second; } } } return -1; } int TrackModel::getClipByRow(int row) const { READ_LOCK(); if (row >= static_cast(m_allClips.size())) { return -1; } auto it = m_allClips.cbegin(); std::advance(it, row); return (*it).first; } std::unordered_set TrackModel::getClipsInRange(int position, int end) { READ_LOCK(); std::unordered_set ids; for (const auto &clp : m_allClips) { int pos = clp.second->getPosition(); int length = clp.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(clp.first); } } return ids; } int TrackModel::getRowfromClip(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return (int)std::distance(m_allClips.begin(), m_allClips.find(clipId)); } +int TrackModel::getTentativeRowfromClip(int clipId) +{ + READ_LOCK(); + Q_ASSERT(m_allClips.count(clipId) == 0); + m_allClips[clipId] = nullptr; + int row = getRowfromClip(clipId); + m_allClips.erase(clipId); + return row; +} + +int TrackModel::getRowfromComposition(int compoId) const +{ + READ_LOCK(); + Q_ASSERT(m_allCompositions.count(compoId) > 0); + return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(compoId)); +} + +int TrackModel::getTentativeRowfromComposition(int compoId) +{ + READ_LOCK(); + Q_ASSERT(m_allCompositions.count(compoId) == 0); + m_allCompositions[compoId] = nullptr; + int row = getRowfromComposition(compoId); + m_allCompositions.erase(compoId); + return row; +} + std::unordered_set TrackModel::getCompositionsInRange(int position, int end) { READ_LOCK(); // TODO: this function doesn't take into accounts the fact that there are two tracks std::unordered_set ids; for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); int length = compo.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(compo.first); } } return ids; } -int TrackModel::getRowfromComposition(int tid) const -{ - READ_LOCK(); - Q_ASSERT(m_allCompositions.count(tid) > 0); - return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(tid)); -} - QVariant TrackModel::getProperty(const QString &name) const { READ_LOCK(); return QVariant(m_track->get(name.toUtf8().constData())); } void TrackModel::setProperty(const QString &name, const QString &value) { QWriteLocker locker(&m_lock); m_track->set(name.toUtf8().constData(), value.toUtf8().constData()); // Hide property mus be defined at playlist level or it won't be saved if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) { for (int i = 0; i < 2; i++) { m_playlists[i].set(name.toUtf8().constData(), value.toInt()); } } } bool TrackModel::checkConsistency() { auto ptr = m_parent.lock(); if (!ptr) { return false; } std::vector> clips; // clips stored by (position, id) for (const auto &c : m_allClips) { Q_ASSERT(c.second); Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get()); clips.push_back({c.second->getPosition(), c.first}); } std::sort(clips.begin(), clips.end()); size_t current_clip = 0; int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime()); for (int i = 0; i < playtime; i++) { int track, index; if (isBlankAt(i)) { track = 0; index = m_playlists[0].get_clip_index_at(i); } else { auto clip_loc = getClipIndexAt(i); track = clip_loc.first; index = clip_loc.second; } Q_ASSERT(m_playlists[(track + 1) % 2].is_blank_at(i)); if (current_clip < clips.size() && i >= clips[current_clip].first) { auto clip = m_allClips[clips[current_clip].second]; if (i >= clips[current_clip].first + clip->getPlaytime()) { current_clip++; i--; continue; } if (isBlankAt(i)) { qDebug() << "ERROR: Found blank when clip was required at position " << i; return false; } auto pr = m_playlists[track].get_clip(index); Mlt::Producer prod(pr); if (!prod.same_clip(*clip)) { qDebug() << "ERROR: Wrong clip at position " << i; delete pr; return false; } delete pr; } else { if (!isBlankAt(i)) { qDebug() << "ERROR: Found clip when blank was required at position " << i; return false; } } } // We now check compositions positions if (m_allCompositions.size() != m_compoPos.size()) { qDebug() << "Error: the number of compositions position doesn't match number of compositions"; return false; } for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); if (m_compoPos.count(pos) == 0) { qDebug() << "Error: the position of composition " << compo.first << " is not properly stored"; return false; } if (m_compoPos[pos] != compo.first) { qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos; return false; } } for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) { int compoId = it->second; int cur_in = m_allCompositions[compoId]->getPosition(); Q_ASSERT(cur_in == it->first); int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1; ++it; if (it != m_compoPos.end()) { int next_compoId = it->second; int next_in = m_allCompositions[next_compoId]->getPosition(); int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1; if (next_in <= cur_out) { qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ " << next_in << ", " << next_out << "]"; return false; } } --it; } return true; } std::pair TrackModel::getClipIndexAt(int position) { READ_LOCK(); for (int j = 0; j < 2; j++) { if (!m_playlists[j].is_blank_at(position)) { return {j, m_playlists[j].get_clip_index_at(position)}; } } Q_ASSERT(false); return {-1, -1}; } bool TrackModel::isBlankAt(int position) { READ_LOCK(); return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position); } int TrackModel::getBlankStart(int position) { READ_LOCK(); int result = 0; for (int j = 0; j < 2; j++) { if (m_playlists[j].count() == 0) { break; } if (!m_playlists[j].is_blank_at(position)) { result = position; break; } int clip_index = m_playlists[j].get_clip_index_at(position); int start = m_playlists[j].clip_start(clip_index); if (start > result) { result = start; } } return result; } int TrackModel::getBlankEnd(int position, int track) { READ_LOCK(); // Q_ASSERT(m_playlists[track].is_blank_at(position)); if (!m_playlists[track].is_blank_at(position)) { return position; } int clip_index = m_playlists[track].get_clip_index_at(position); int count = m_playlists[track].count(); if (clip_index < count) { int blank_start = m_playlists[track].clip_start(clip_index); int blank_length = m_playlists[track].clip_length(clip_index); return blank_start + blank_length; } return INT_MAX; } int TrackModel::getBlankEnd(int position) { READ_LOCK(); int end = INT_MAX; for (int j = 0; j < 2; j++) { end = std::min(getBlankEnd(position, j), end); } return end; } Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out) { QWriteLocker locker(&m_lock); int compo_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(compo_position) > 0); Q_ASSERT(m_compoPos[compo_position] == compoId); int old_in = compo_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime(); qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out; if (out == -1) { out = in + old_out - old_in; } auto update_snaps = [compoId, old_in, old_out, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); // ptr->adjustAssetRange(compoId, new_in, new_out); } else { qDebug() << "Error : Composition resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; if (in == compo_position && (out == -1 || out == old_out)) { return []() { return true; }; } // temporary remove of current compo to check collisions m_compoPos.erase(compo_position); bool intersecting = hasIntersectingComposition(in, out); // put it back m_compoPos[compo_position] = compoId; if (intersecting) { return []() { return false; }; } return [in, out, compoId, update_snaps, this]() { m_compoPos.erase(m_allCompositions[compoId]->getPosition()); m_allCompositions[compoId]->setInOut(in, out); update_snaps(in, out + 1); m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId; return true; }; } -bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, Fun &undo, Fun &redo) +bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } auto operation = requestCompositionInsertion_lambda(compoId, position, updateView); if (operation()) { + if (updateView) { + list.emplace_back(std::make_shared(compoId, m_parent, getId(), position, false)); + } auto reverse = requestCompositionDeletion_lambda(compoId, updateView); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } -bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, Fun &undo, Fun &redo) +bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, Fun &undo, Fun &redo, Updates &list) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } Q_ASSERT(m_allCompositions.count(compoId) > 0); auto old_composition = m_allCompositions[compoId]; int old_position = old_composition->getPosition(); Q_ASSERT(m_compoPos.count(old_position) > 0); Q_ASSERT(m_compoPos[old_position] == compoId); auto operation = requestCompositionDeletion_lambda(compoId, updateView); if (operation()) { + if (updateView) { + list.emplace_back(std::make_shared(compoId, m_parent, getId(), old_position, false)); + } auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allCompositions[compoId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime(); return [clip_position, compoId, old_in, old_out, updateView, this]() { - int old_clip_index = getRowfromComposition(compoId); auto ptr = m_parent.lock(); if (updateView) { - ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); - ptr->_endRemoveRows(); + if (!isHidden() && !isAudioTrack()) { + // only refresh monitor if not an audio track and not hidden + ptr->checkRefresh(old_in, old_out); + } } m_allCompositions[compoId]->setCurrentTrackId(-1); m_allCompositions.erase(compoId); m_compoPos.erase(old_in); ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); return true; }; } int TrackModel::getCompositionByRow(int row) const { READ_LOCK(); if (row < (int)m_allClips.size()) { return -1; } // Q_ASSERT(row <= (int)m_allClips.size() + m_allCompositions.size()); auto it = m_allCompositions.cbegin(); std::advance(it, row - (int)m_allClips.size()); return (*it).first; } int TrackModel::getCompositionsCount() const { READ_LOCK(); return (int)m_allCompositions.size(); } Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView) { QWriteLocker locker(&m_lock); bool intersecting = true; if (auto ptr = m_parent.lock()) { intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1); } else { qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; } if (!intersecting) { return [compoId, this, position, updateView]() { if (auto ptr = m_parent.lock()) { std::shared_ptr composition = ptr->getCompositionPtr(compoId); m_allCompositions[composition->getId()] = composition; // store clip // update clip position and track composition->setCurrentTrackId(getId()); int new_in = position; int new_out = new_in + composition->getPlaytime(); composition->setInOut(new_in, new_out - 1); if (updateView) { - int composition_index = getRowfromComposition(composition->getId()); - ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index); - ptr->_endInsertRows(); + if (!isHidden() && !isAudioTrack()) { + // only refresh monitor if not an audio track and not hidden + ptr->checkRefresh(new_in, new_out); + } } ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); m_compoPos[new_in] = composition->getId(); return true; } qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; return false; }; } return []() { return false; }; } bool TrackModel::hasIntersectingComposition(int in, int out) const { READ_LOCK(); auto it = m_compoPos.lower_bound(in); if (m_compoPos.empty()) { return false; } if (it != m_compoPos.end() && it->first <= out) { // compo at it intersects return true; } if (it == m_compoPos.begin()) { return false; } --it; int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1; return end >= in; return false; } bool TrackModel::addEffect(const QString &effectId) { READ_LOCK(); m_effectStack->appendEffect(effectId); return true; } const QString TrackModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } bool TrackModel::stackEnabled() const { READ_LOCK(); return m_effectStack->isStackEnabled(); } void TrackModel::setEffectStackEnabled(bool enable) { m_effectStack->setEffectStackEnabled(enable); } int TrackModel::trackDuration() { return m_track->get_length(); } bool TrackModel::isLocked() const { READ_LOCK(); return m_track->get_int("kdenlive:locked_track"); } bool TrackModel::isAudioTrack() const { return m_track->get_int("kdenlive:audio_track") == 1; } bool TrackModel::isHidden() const { return m_track->get_int("hide") & 1; } bool TrackModel::isMute() const { return m_track->get_int("hide") & 2; } bool TrackModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(service); return true; } diff --git a/src/timeline2/model/trackmodel.hpp b/src/timeline2/model/trackmodel.hpp index e92698bdc..6619039ee 100644 --- a/src/timeline2/model/trackmodel.hpp +++ b/src/timeline2/model/trackmodel.hpp @@ -1,264 +1,275 @@ /*************************************************************************** * 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 TRACKMODEL_H #define TRACKMODEL_H +#include "modelupdater.hpp" #include "undohelper.hpp" #include #include #include #include #include #include #include class TimelineModel; class ClipModel; class CompositionModel; class EffectStackModel; /* @brief This class represents a Track object, as viewed by the backend. To allow same track transitions, a Track object corresponds to two Mlt::Playlist, between which we can switch when required by the transitions. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class TrackModel { public: TrackModel() = delete; ~TrackModel(); friend class ClipModel; friend class CompositionModel; friend class TimelineController; friend struct TimelineFunctions; friend class TimelineItemModel; friend class TimelineModel; + friend class ModelUpdater; private: /* This constructor is private, call the static construct instead */ TrackModel(const std::weak_ptr &parent, int id = -1, const QString &trackName = QString(), bool audioTrack = false); TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id = -1); public: /* @brief Creates a track, which references itself to the parent Returns the (unique) id of the created track @param id Requested id of the track. Automatic if id = -1 @param pos is the optional position of the track. If left to -1, it will be added at the end */ static int construct(const std::weak_ptr &parent, int id = -1, int pos = -1, const QString &trackName = QString(), bool audioTrack = false); /* @brief returns the number of clips */ int getClipsCount(); /* @brief returns the number of compositions */ int getCompositionsCount() const; /* Perform a split at the requested position */ bool splitClip(QSharedPointer caller, int position); /* Implicit conversion operator to access the underlying producer */ operator Mlt::Producer &() { return *m_track.get(); } /* @brief Returns true if track is in locked state */ bool isLocked() const; /* @brief Returns true if track is an audio track */ bool isAudioTrack() const; /* @brief Returns true if track is disabled */ bool isHidden() const; /* @brief Returns true if track is disabled */ bool isMute() const; // TODO make protected QVariant getProperty(const QString &name) const; void setProperty(const QString &name, const QString &value); protected: /* @brief Returns a lambda that performs a resize of the given clip. The lamda returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param in is the new starting on the clip @param out is the new ending on the clip @param right is true if we change the right side of the clip, false otherwise */ Fun requestClipResize_lambda(int clipId, int in, int out, bool right); /* @brief Performs an insertion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clip is the id of the clip @param position is the position where to insert the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); + bool requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, Updates &list); /* @brief This function returns a lambda that performs the requested operation */ Fun requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove); /* @brief Performs an deletion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo); + bool requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, Updates &list); /* @brief This function returns a lambda that performs the requested operation */ Fun requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove); /* @brief Performs an insertion of the given composition. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. Note that in Mlt, the composition insertion logic is not really at the track level, but we use that level to do collision checking @param compoId is the id of the composition @param position is the position where to insert the composition @param updateView whether we send update to the view @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ - bool requestCompositionInsertion(int compoId, int position, bool updateView, Fun &undo, Fun &redo); + bool requestCompositionInsertion(int compoId, int position, bool updateView, Fun &undo, Fun &redo, Updates &list); /* @brief This function returns a lambda that performs the requested operation */ Fun requestCompositionInsertion_lambda(int compoId, int position, bool updateView); - bool requestCompositionDeletion(int compoId, bool updateView, Fun &undo, Fun &redo); + bool requestCompositionDeletion(int compoId, bool updateView, Fun &undo, Fun &redo, Updates &list); Fun requestCompositionDeletion_lambda(int compoId, bool updateView); Fun requestCompositionResize_lambda(int compoId, int in, int out = -1); /* @brief Returns the size of the blank before or after the given clip @param clipId is the id of the clip @param after is true if we query the blank after, false otherwise */ int getBlankSizeNearClip(int clipId, bool after); int getBlankSizeNearComposition(int compoId, bool after); int getBlankStart(int position); int getBlankSizeAtPos(int frame); /*@brief Returns the (unique) construction id of the track*/ int getId() const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding clip id. If it does not exist, returns -1 */ int getClipByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding composition id. If it does not exist, returns -1 */ int getCompositionByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a clip ID, returns the row of the clip. */ int getRowfromClip(int clipId) const; + /*@brief This function is used only by the QAbstractItemModel + Given a clip ID that is not inserted yet, returns the row on which that clip would be inserted. + */ + int getTentativeRowfromClip(int clipId); + /*@brief This function is used only by the QAbstractItemModel Given a composition ID, returns the row of the composition. */ int getRowfromComposition(int compoId) const; + /*@brief This function is used only by the QAbstractItemModel + Given a composition ID that is not inserted yet, returns the row on which that composition would be inserted. + */ + int getTentativeRowfromComposition(int compoId); /*@brief This is an helper function that test frame level consistancy with the MLT structures */ bool checkConsistency(); /* @brief Returns true if we have a composition intersecting with the range [in,out]*/ bool hasIntersectingComposition(int in, int out) const; /* @brief This is an helper function that returns the sub-playlist in which the clip is inserted, along with its index in the playlist @param position the position of the target clip*/ std::pair getClipIndexAt(int position); /* @brief This is an helper function that checks in all playlists if the given position is a blank */ bool isBlankAt(int position); /* @brief This is an helper function that returns the end of the blank that covers given position */ int getBlankEnd(int position); /* Same, but we restrict to a specific track*/ int getBlankEnd(int position, int track); /* @brief Returns the clip id on this track at position requested, or -1 if no clip */ int getClipByPosition(int position); /* @brief Returns the composition id on this track starting position requested, or -1 if not found */ int getCompositionByPosition(int position); /* @brief Add a track effect */ bool addEffect(const QString &effectId); /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; /* @brief Returns true if effect stack is enabled */ bool stackEnabled() const; /* @brief Enable / disable the track's effect stack */ void setEffectStackEnabled(bool enable); /* @brief This function removes the clip from the mlt object, and then insert it back in the same spot again. * This is used when some properties of the clip have changed, and we need this to refresh it */ void replugClip(int clipId); int trackDuration(); /* @brief Returns the list of the ids of the clips that intersect the given range */ std::unordered_set getClipsInRange(int position, int end = -1); /* @brief Returns the list of the ids of the compositions that intersect the given range */ std::unordered_set getCompositionsInRange(int position, int end); /* @brief Import effects from a service that contains some (another track) */ bool importEffects(std::weak_ptr service); public slots: /*Delete the current track and all its associated clips */ void slotDelete(); private: std::weak_ptr m_parent; int m_id; // this is the creation id of the track, used for book-keeping // We fake two playlists to allow same track transitions. std::shared_ptr m_track; Mlt::Playlist m_playlists[2]; std::map> m_allClips; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map> m_allCompositions; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map m_compoPos; // We store the positions of the compositions. In Melt, the compositions are not inserted at the track level, but we keep // those positions here to check for moves and resize mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access protected: std::shared_ptr m_effectStack; }; #endif diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 70a82a945..d8ad5f0c9 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,1769 +1,1771 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * 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 "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "bin/bin.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "timelinewidget.h" #include "transitions/transitionsrepository.hpp" - #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_zone(-1, -1) , m_scale(3.0) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); m_disablePreview->setEnabled(false); } TimelineController::~TimelineController() { delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); m_selection.selectedItems.clear(); m_selection.selectedTrack = -1; connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::removeFromSelection, this, &TimelineController::slotUpdateSelection); } void TimelineController::setTargetTracks(QPair targets) { setVideoTarget(targets.first >= 0 ? m_model->getTrackIndexFromPosition(targets.first) : -1); setAudioTarget(targets.second >= 0 ? m_model->getTrackIndexFromPosition(targets.second) : -1); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } void TimelineController::addSelection(int newSelection) { if (m_selection.selectedItems.contains(newSelection)) { return; } std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems << newSelection; std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); std::unordered_set newIds; if (m_model->m_temporarySelectionGroup >= 0) { // new items were selected, inform model to prepare for group drag newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } emit selectionChanged(); if (!m_selection.selectedItems.isEmpty()) emitSelectedFromSelection(); else emit selected(nullptr); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor if (m_selection.selectedItems.isEmpty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection return m_selection.selectedItems.constFirst(); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById(track.first)->getProperty(QStringLiteral("kdenlive:audio_track")).toInt() == 1) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ m_scale = scale; emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } std::unordered_set TimelineController::getCurrentSelectionIds() const { std::unordered_set selection; if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { selection = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { for (int i : m_selection.selectedItems) { selection.insert(i); } } return selection; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, timelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (addToCurrent || !select) { toSelect = m_selection.selectedItems; } if (select) { if (!toSelect.contains(currentClip)) { toSelect << currentClip; setSelection(toSelect); } } else if (toSelect.contains(currentClip)) { toSelect.removeAll(currentClip); setSelection(toSelect); } } void TimelineController::setSelection(const QList &newSelection, int trackIndex, bool isMultitrack) { if (newSelection != selection() || trackIndex != m_selection.selectedTrack || isMultitrack != m_selection.isMultitrackSelected) { qDebug() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack; std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems = newSelection; m_selection.selectedTrack = trackIndex; m_selection.isMultitrackSelected = isMultitrack; if (m_model->m_temporarySelectionGroup > -1) { // Clear current selection m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; if (m_selection.selectedItems.size() > 0) { std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { qDebug() << "// NON GROUPED SELCTUIIN: " << m_selection.selectedItems << " !!!!!!"; } emitSelectedFromSelection(); } else { // Empty selection emit selected(nullptr); emit showItemEffectStack(QString(), nullptr, QSize(), false); } emit selectionChanged(); } } void TimelineController::emitSelectedFromSelection() { /*if (!m_model.trackList().count()) { if (m_model.tractor()) selectMultitrack(); else emit selected(0); return; } int trackIndex = currentTrack(); int clipIndex = selection().isEmpty()? 0 : selection().first(); Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex); if (info && info->producer && info->producer->is_valid()) { delete m_updateCommand; m_updateCommand = new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start); // We need to set these special properties so time-based filters // can get information about the cut while still applying filters // to the cut parent. info->producer->set(kFilterInProperty, info->frame_in); info->producer->set(kFilterOutProperty, info->frame_out); if (MLT.isImageProducer(info->producer)) info->producer->set("out", info->cut->get_int("out")); info->producer->set(kMultitrackItemProperty, 1); m_ignoreNextPositionChange = true; emit selected(info->producer); delete info; }*/ } QList TimelineController::selection() const { if (!m_root) return QList(); return m_selection.selectedItems; } void TimelineController::selectMultitrack() { setSelection(QList(), -1, true); QMetaObject::invokeMethod(m_root, "selectMultitrack"); // emit selected(m_model.tractor()); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } void TimelineController::snapChanged(bool snap) { m_root->setProperty("snapping", snap ? 10 / std::sqrt(m_scale) : -1); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; if (!m_model->requestCompositionInsertion(transitionId, tid, position, 100, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { if (m_selection.selectedItems.isEmpty()) { return; } if (m_model->m_temporarySelectionGroup != -1) { // selection is grouped, delete group only m_model->requestGroupDeletion(m_model->m_temporarySelectionGroup); } else { for (int cid : m_selection.selectedItems) { m_model->requestItemDeletion(cid); } } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::slotUpdateSelection(int itemId) { if (m_selection.selectedItems.contains(itemId)) { m_selection.selectedItems.removeAll(itemId); emit selectionChanged(); } } void TimelineController::copyItem() { int clipId = -1; if (!m_selection.selectedItems.isEmpty()) { clipId = m_selection.selectedItems.first(); } else { return; } m_root->setProperty("copiedClip", clipId); } bool TimelineController::pasteItem(int clipId, int tid, int position) { // TODO: copy multiple clips / groups if (clipId == -1) { clipId = m_root->property("copiedClip").toInt(); if (clipId == -1) { return -1; } } if (tid == -1 && position == -1) { tid = getMouseTrack(); position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } qDebug() << "PASTING CLIP: " << clipId << ", " << tid << ", " << position; return TimelineFunctions::requestItemCopy(m_model, clipId, tid, position); return false; } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int newTid; m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); m_model->buildTrackCompositing(true); } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); m_model->buildTrackCompositing(true); if (m_activeTrack == selectedTrackIx) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::gotoNextSnap() { setPosition(m_model->requestNextSnapPos(timelinePosition())); } void TimelineController::gotoPreviousSnap() { setPosition(m_model->requestPreviousSnapPos(timelinePosition())); } void TimelineController::groupSelection() { if (m_selection.selectedItems.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } std::unordered_set clips; for (int id : m_selection.selectedItems) { clips.insert(id); } m_model->requestClipsGroup(clips); } void TimelineController::unGroupSelection(int cid) { if (cid == -1 && m_selection.selectedItems.isEmpty()) { pCore->displayMessage(i18n("Select at least 1 item to ungroup"), InformationMessage, 500); return; } if (cid == -1) { if (m_model->m_temporarySelectionGroup >= 0) { cid = m_model->m_temporarySelectionGroup; } else { for (int id : m_selection.selectedItems) { if (m_model->m_groups->getRootId(id)) { cid = id; break; } } } } if (cid > -1) { if (m_model->m_groups->isInGroup(cid)) { cid = m_model->m_groups->getRootId(cid); } m_model->requestClipUngroup(cid); } if (m_model->m_temporarySelectionGroup >= 0) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::setInPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); } } } void TimelineController::editMarker(const QString &cid, int frame) { std::shared_ptr clip = pCore->bin()->getBinClip(cid); GenTime pos(frame, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = timelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = timelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); if (!m_selection.selectedItems.isEmpty()) { QList effectSelection; for (int id : m_selection.selectedItems) { if (m_model->isClip(id)) { effectSelection << id; int partner = m_model->getClipSplitPartner(id); if (partner > -1 && !effectSelection.contains(partner)) { effectSelection << partner; } } } for (int id : effectSelection) { m_model->addClipEffect(id, effect); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setSeekPosition(int position) { m_seekPosition = position; emit seekPositionChanged(); } void TimelineController::onSeeked(int position) { m_position = position; emit positionChanged(); if (m_seekPosition > -1 && position == m_seekPosition) { m_seekPosition = -1; emit seekPositionChanged(); } } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } void TimelineController::selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect) { std::unordered_set previousSelection = getCurrentSelectionIds(); std::unordered_set itemsToSelect; if (addToSelect) { for (int cid : m_selection.selectedItems) { itemsToSelect.insert(cid); } } m_selection.selectedItems.clear(); for (int i = 0; i < arg.count(); i++) { auto currentClips = m_model->getItemsInRange(arg.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } if (itemsToSelect.size() > 0) { for (int x : itemsToSelect) { m_selection.selectedItems << x; } qDebug() << "// GROUPING ITEMS: " << m_selection.selectedItems; m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(itemsToSelect, true, GroupType::Selection); qDebug() << "// GROUPING ITEMS DONE"; } else if (m_model->m_temporarySelectionGroup > -1) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; if (m_model->m_temporarySelectionGroup >= 0) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } emit selectionChanged(); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = timelinePosition(); } bool foundClip = false; for (int cid : m_selection.selectedItems) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); } void TimelineController::seekCurrentClip(bool seekToEnd) { for (int cid : m_selection.selectedItems) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); if (in > m_position) { return; } if (m_position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, timelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(Mlt::Filter *filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } int clipId = getCurrentItem(); if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer Mlt::Producer *clipProducer = new Mlt::Producer(*clip); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist Mlt::Playlist *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_timelinePreview && !m_zone.isNull()) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); initializePreview(); } } void TimelineController::loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(timelinePosition())); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } // find blank duration int spaceDuration = m_model->getTrackById(trackId)->getBlankSizeAtPos(frame); int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start - spaceDuration); } void TimelineController::invalidateClip(int cid) { if (!m_timelinePreview) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, double speed) { if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); bool ok = false; double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)) - 1); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)) - 1); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok); if (!ok) { return; } } m_model->requestClipTimeWarp(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); qDebug() << "//// SWITCH COMPO: " << mode; QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 1; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, QVector() << track, zone, false); } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int targetTrack = -1; if (audioTarget() == -1 && videoTarget() == -1) { // No target tracks defined, use active track targetTrack = m_activeTrack; } else if (clip->clipType() == ClipType::Audio) { // Audio clip, only allowed on audio track targetTrack = audioTarget(); } else { // Video clip targetTrack = videoTarget(); if (targetTrack == -1 && clip->hasAudio()) { // No video target defined, switch to audio if available targetTrack = audioTarget(); } } int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use curent timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } return TimelineFunctions::insertZone(m_model, targetTrack, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x()) : -1; } void TimelineController::updateClip(int clipId, QVector roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(int clipId) { TimelineFunctions::switchEnableState(m_model, clipId); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId) { int position = m_model->getItemPosition(clipId); int duration = m_model->getItemPlaytime(clipId); int track = m_model->getItemTrackId(clipId); int id; m_model->requestCompositionInsertion(assetId, track, position, duration, nullptr, id, true); } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = m_selection.selectedItems.first(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:locked_track").toInt() == 1; m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1")); } else { // Invert track lock // Get track states first QMap trackLockState; int unlockedTracksCount = 0; int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1; if (!isLocked) { unlockedTracksCount++; } trackLockState.insert(trackIx, isLocked); } if (unlockedTracksCount == tracksCount) { // do not lock all tracks, leave active track unlocked trackLockState.insert(m_activeTrack, true); } QMapIterator i(trackLockState); while (i.hasNext()) { i.next(); m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1")); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } int TimelineController::groupClips(const QList &clipIds) { std::unordered_set theSet(clipIds.begin(), clipIds.end()); return m_model->requestClipsGroup(theSet, false, GroupType::Selection); } bool TimelineController::ungroupClips(int clipId) { return m_model->requestClipUngroup(clipId); } void TimelineController::clearSelection() { if (m_model->m_temporarySelectionGroup >= 0) { m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); m_model->m_temporarySelectionGroup = -1; } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::selectAll() { QList ids; for (auto clp : m_model->m_allClips) { ids << clp.first; } for (auto clp : m_model->m_allCompositions) { ids << clp.first; } setSelection(ids); } void TimelineController::selectCurrentTrack() { QList ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids << clp.first; } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids << clp.first; } setSelection(ids); } void TimelineController::pasteEffects(int targetId, int sourceId) { if (!m_model->isClip(targetId) || !m_model->isClip(sourceId)) { return; } std::shared_ptr sourceStack = m_model->getClipEffectStackModel(sourceId); std::shared_ptr destStack = m_model->getClipEffectStackModel(targetId); destStack->importEffects(sourceStack); } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); if (result && partner > -1) { - result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); + result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo, list); } } else { - result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, undo, redo); + result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, undo, redo, list); } if (result && newIn != in) { - m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); + m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo, list); if (result && partner > -1) { - result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); + result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo, list); } } if (newDuration != duration + (in - newIn)) { - result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); + result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo, list); if (result && partner > -1) { - result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); + result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo, list); } } } else { // perform resize first if (newIn != in) { - result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); + result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo, list); if (result && partner > -1) { - result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); + result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo, list); } } if (newDuration != duration + (in - newIn)) { - result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); + result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo, list); if (result && partner > -1) { - result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); + result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo, list); } } if (start != newPos || newIn != in) { if (!isComposition) { - result = result && m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); + result = result && m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo, list); if (result && partner > -1) { - result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); + result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo, list); } } else { - result = result && m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, undo, redo); + result = result && + m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, undo, redo, list); } } } if (result) { + ModelUpdater::applyUpdates(undo, redo, list); pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_selection.selectedItems.isEmpty()) { - for(QAction *act : clipActions) { + for (QAction *act : clipActions) { act->setEnabled(false); } return; } std::shared_ptr clip(nullptr); int item = m_selection.selectedItems.first(); if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } - for(QAction *act : clipActions) { + for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = m_selection.selectedItems.size() > 1; } else if (actionData == QLatin1Char('U')) { enableAction = m_model->m_groups->isInGroup(item) && !m_model->isInSelection(item); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } act->setEnabled(enableAction); } } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { if (m_selection.selectedItems.isEmpty()) { //TODO: error displayMessage return; } int id = m_selection.selectedItems.constFirst(); if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeClipIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeCompositionIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } } diff --git a/tests/groupstest.cpp b/tests/groupstest.cpp index 9a8587d06..a56d435a1 100644 --- a/tests/groupstest.cpp +++ b/tests/groupstest.cpp @@ -1,1285 +1,1296 @@ #include "bin/model/markerlistmodel.hpp" #include "catch.hpp" #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #pragma GCC diagnostic push #include "fakeit.hpp" #include #include #define private public #define protected public #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/docundostack.hpp" #include "project/projectmanager.h" #include "test_utils.hpp" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/timelinemodel.hpp" #include "timeline2/model/trackmodel.hpp" #include #include Mlt::Profile profile_group; TEST_CASE("Functional test of the group hierarchy", "[GroupsModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; for (int i = 0; i < 10; i++) { groups.createGroupItem(i); } SECTION("Test Basic Creation") { for (int i = 0; i < 10; i++) { REQUIRE(groups.getRootId(i) == i); REQUIRE(groups.isLeaf(i)); REQUIRE(groups.getLeaves(i).size() == 1); REQUIRE(groups.getSubtree(i).size() == 1); } } groups.setGroup(0, 1); groups.setGroup(1, 2); groups.setGroup(3, 2); groups.setGroup(9, 3); groups.setGroup(6, 3); groups.setGroup(4, 3); groups.setGroup(7, 3); groups.setGroup(8, 5); SECTION("Test leaf nodes") { std::unordered_set nodes = {1, 2, 3, 5}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving") { REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(5) == std::unordered_set({8})); } SECTION("Test subtree retrieving") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8})); } SECTION("Test root retieving") { std::set first_tree = {0, 1, 2, 3, 4, 6, 7, 9}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {5, 8}; for (int n : second_tree) { REQUIRE(groups.getRootId(n) == 5); } } groups.setGroup(3, 8); SECTION("Test leaf nodes 2") { std::unordered_set nodes = {1, 2, 3, 5, 8}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 2") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 2") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9})); } SECTION("Test root retieving 2") { std::set first_tree = {0, 1, 2}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {5, 8, 3, 4, 6, 7, 9}; for (int n : second_tree) { REQUIRE(groups.getRootId(n) == 5); } } groups.setGroup(5, 2); SECTION("Test leaf nodes 3") { std::unordered_set nodes = {1, 2, 3, 5, 8}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 3") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 3") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9})); } SECTION("Test root retieving 3") { for (int i = 0; i < 10; i++) { CAPTURE(i); REQUIRE(groups.getRootId(i) == 2); } } - groups.destructGroupItem(8, false, undo, redo); + groups.destructGroupItem(8, false, undo, redo, list); SECTION("Test leaf nodes 4") { std::unordered_set nodes = {1, 2, 3}; for (int i = 0; i < 10; i++) { if (i == 8) continue; REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 4") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 5})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 4") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 5})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5})); } SECTION("Test root retieving 4") { std::set first_tree = {0, 1, 2, 5}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {3, 4, 6, 7, 9}; for (int n : second_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 3); } } } TEST_CASE("Interface test of the group hierarchy", "[GroupsModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; for (int i = 0; i < 10; i++) { groups.createGroupItem(i); // the following call shouldn't do anything, but we test that behaviour too. - groups.ungroupItem(i, undo, redo); + groups.ungroupItem(i, undo, redo, list); REQUIRE(groups.getRootId(i) == i); REQUIRE(groups.isLeaf(i)); REQUIRE(groups.getLeaves(i).size() == 1); REQUIRE(groups.getSubtree(i).size() == 1); REQUIRE(groups.checkConsistency(false)); } auto g1 = std::unordered_set({4, 6, 7, 9}); - int gid1 = groups.groupItems(g1, undo, redo); + int gid1 = groups.groupItems(g1, undo, redo, list); SECTION("One single group") { for (int i = 0; i < 10; i++) { if (g1.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid1); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); auto g1b = g1; g1b.insert(gid1); REQUIRE(groups.getSubtree(gid1) == g1b); REQUIRE(groups.checkConsistency(false)); } SECTION("Twice the same group") { int old_gid1 = gid1; - gid1 = groups.groupItems(g1, undo, redo); // recreate the same group (will create a parent with the old group as only element) + gid1 = groups.groupItems(g1, undo, redo, list); // recreate the same group (will create a parent with the old group as only element) for (int i = 0; i < 10; i++) { if (g1.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid1); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(old_gid1) == g1); auto g1b = g1; g1b.insert(old_gid1); REQUIRE(groups.getSubtree(old_gid1) == g1b); g1b.insert(gid1); REQUIRE(groups.getSubtree(gid1) == g1b); REQUIRE(groups.checkConsistency(false)); } auto g2 = std::unordered_set({3, 5, 7}); - int gid2 = groups.groupItems(g2, undo, redo); + int gid2 = groups.groupItems(g2, undo, redo, list); auto all_g2 = g2; all_g2.insert(4); all_g2.insert(6); all_g2.insert(9); SECTION("Heterogeneous group") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g2.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid2); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(gid2) == all_g2); REQUIRE(groups.checkConsistency(false)); } auto g3 = std::unordered_set({0, 1}); - int gid3 = groups.groupItems(g3, undo, redo); + int gid3 = groups.groupItems(g3, undo, redo, list); auto g4 = std::unordered_set({0, 4}); - int gid4 = groups.groupItems(g4, undo, redo); + int gid4 = groups.groupItems(g4, undo, redo, list); auto all_g4 = all_g2; for (int i : g3) all_g4.insert(i); SECTION("Group of group") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g4.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid4); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(gid2) == all_g2); REQUIRE(groups.getLeaves(gid3) == g3); REQUIRE(groups.getLeaves(gid4) == all_g4); REQUIRE(groups.checkConsistency(false)); } // the following should delete g4 - groups.ungroupItem(3, undo, redo); + groups.ungroupItem(3, undo, redo, list); SECTION("Ungroup") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g2.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid2); } else if (g3.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid3); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.checkConsistency(false)); REQUIRE(groups.getLeaves(gid2) == all_g2); REQUIRE(groups.getLeaves(gid3) == g3); } } TEST_CASE("Orphan groups deletion", "[GroupsModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; for (int i = 0; i < 4; i++) { groups.createGroupItem(i); } auto g1 = std::unordered_set({0, 1}); - int gid1 = groups.groupItems(g1, undo, redo); + int gid1 = groups.groupItems(g1, undo, redo, list); auto g2 = std::unordered_set({2, 3}); - int gid2 = groups.groupItems(g2, undo, redo); + int gid2 = groups.groupItems(g2, undo, redo, list); auto g3 = std::unordered_set({0, 3}); - int gid3 = groups.groupItems(g3, undo, redo); + int gid3 = groups.groupItems(g3, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({0, 1, 2, 3})); REQUIRE(groups.checkConsistency(false)); - groups.destructGroupItem(0, true, undo, redo); + groups.destructGroupItem(0, true, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({1, 2, 3})); REQUIRE(groups.checkConsistency(false)); SECTION("Normal deletion") { - groups.destructGroupItem(1, false, undo, redo); + groups.destructGroupItem(1, false, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({gid1, 2, 3})); REQUIRE(groups.checkConsistency(false)); - groups.destructGroupItem(gid1, true, undo, redo); + groups.destructGroupItem(gid1, true, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3})); REQUIRE(groups.checkConsistency(false)); } SECTION("Cascade deletion") { - groups.destructGroupItem(1, true, undo, redo); + groups.destructGroupItem(1, true, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3})); REQUIRE(groups.checkConsistency(false)); - groups.destructGroupItem(2, true, undo, redo); + groups.destructGroupItem(2, true, undo, redo, list); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({3})); REQUIRE(groups.checkConsistency(false)); REQUIRE(groups.m_downLink.count(gid3) > 0); - groups.destructGroupItem(3, true, undo, redo); + groups.destructGroupItem(3, true, undo, redo, list); REQUIRE(groups.m_downLink.count(gid3) == 0); REQUIRE(groups.m_downLink.size() == 0); REQUIRE(groups.checkConsistency(false)); } } TEST_CASE("Integration with timeline", "[GroupsModel]") { qDebug() << "STARTING PASS"; auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); TimelineItemModel tim2(new Mlt::Profile(), undoStack); Mock timMock2(tim2); TimelineItemModel &tt2 = timMock2.get(); auto timeline2 = std::shared_ptr(&timMock2.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline2, guideModel); RESET(timMock); RESET(timMock2); Mlt::Profile *pr = new Mlt::Profile(); QString binId = createProducer(*pr, "red", binModel); QString binId2 = createProducerWithSound(*pr, binModel); int length = binModel->getClipByBinID(binId)->frameDuration(); GroupsModel groups(timeline); std::vector clips; for (int i = 0; i < 4; i++) { if (i % 2 == 0) { clips.push_back(ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly)); } else { clips.push_back(ClipModel::construct(timeline, binId, -1, PlaylistState::AudioOnly)); timeline->m_allClips[clips.back()]->m_canBeAudio = true; } } std::vector clips2; for (int i = 0; i < 4; i++) { if (i % 2 == 0) { clips2.push_back(ClipModel::construct(timeline2, binId, -1, PlaylistState::VideoOnly)); } else { clips2.push_back(ClipModel::construct(timeline2, binId, -1, PlaylistState::AudioOnly)); timeline2->m_allClips[clips2.back()]->m_canBeAudio = true; } } int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline, -1, -1, QStringLiteral("audio"), true); int tid1_2 = TrackModel::construct(timeline2); int tid2_2 = TrackModel::construct(timeline2); int tid3_2 = TrackModel::construct(timeline2, -1, -1, QStringLiteral("audio2"), true); int init_index = undoStack->index(); SECTION("Basic Creation and export/import from json") { auto check_roots = [&](int r1, int r2, int r3, int r4) { REQUIRE(timeline->m_groups->getRootId(clips[0]) == r1); REQUIRE(timeline->m_groups->getRootId(clips[1]) == r2); REQUIRE(timeline->m_groups->getRootId(clips[2]) == r3); REQUIRE(timeline->m_groups->getRootId(clips[3]) == r4); }; // the following function is a recursive function to check the correctness of a json import // Basically, it takes as input a groupId in the imported (target) group hierarchy, and outputs the corresponding groupId from the original one. If no // match is found, it returns -1 std::function rec_check; rec_check = [&](int gid) { // we first check if the gid is a leaf if (timeline2->m_groups->isLeaf(gid)) { // then it must be a clip/composition int found = -1; for (int i = 0; i < 4; i++) { if (clips2[i] == gid) { found = i; break; } } if (found != -1) { return clips[found]; } else { qDebug() << "ERROR: did not find correspondance for group" << gid; } } else { // we find correspondances of all the children auto children = timeline2->m_groups->getDirectChildren(gid); std::unordered_set corresp; for (int c : children) { corresp.insert(rec_check(c)); } if (corresp.count(-1) > 0) { return -1; // something went wrong } std::unordered_set parents; for (int c : corresp) { // we find the parents of the corresponding groups in the original hierarchy parents.insert(timeline->m_groups->m_upLink[c]); } // if the matching is correct, we should have found only one parent if (parents.size() != 1) { return -1; // something went wrong } return *parents.begin(); } return -1; }; auto checkJsonParsing = [&]() { // we first destroy all groups in target timeline Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; for (int i = 0; i < 4; i++) { while (timeline2->m_groups->getRootId(clips2[i]) != clips2[i]) { - timeline2->m_groups->ungroupItem(clips2[i], undo, redo); + timeline2->m_groups->ungroupItem(clips2[i], undo, redo, list); } } // we do the export then import REQUIRE(timeline2->m_groups->fromJson(timeline->m_groups->toJson())); std::unordered_map roots; for (int i = 0; i < 4; i++) { int r = timeline2->m_groups->getRootId(clips2[0]); if (roots.count(r) == 0) { roots[r] = rec_check(r); REQUIRE(roots[r] != -1); } } for (int i = 0; i < 4; i++) { int r = timeline->m_groups->getRootId(clips[0]); int r2 = timeline2->m_groups->getRootId(clips2[0]); REQUIRE(roots[r2] == r); } REQUIRE(timeline->checkConsistency()); REQUIRE(timeline2->checkConsistency()); }; REQUIRE(timeline->checkConsistency()); REQUIRE(timeline2->checkConsistency()); auto g1 = std::unordered_set({clips[0], clips[1]}); int gid1, gid2, gid3; // this fails because clips are not inserted REQUIRE(timeline->requestClipsGroup(g1) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline2->checkConsistency()); for (int i = 0; i < 4; i++) { REQUIRE(timeline->requestClipMove(clips[i], (i % 2 == 0) ? tid1 : tid3, i * length)); } for (int i = 0; i < 4; i++) { REQUIRE(timeline2->requestClipMove(clips2[i], (i % 2 == 0) ? tid1_2 : tid3_2, i * length)); } REQUIRE(timeline->checkConsistency()); REQUIRE(timeline2->checkConsistency()); init_index = undoStack->index(); REQUIRE(timeline->requestClipsGroup(g1, true, GroupType::Normal) > 0); auto state1 = [&]() { gid1 = timeline->m_groups->getRootId(clips[0]); check_roots(gid1, gid1, clips[2], clips[3]); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->checkConsistency()); }; INFO("Test 1"); state1(); checkJsonParsing(); state1(); auto g2 = std::unordered_set({clips[2], clips[3]}); REQUIRE(timeline->requestClipsGroup(g2, true, GroupType::AVSplit) > 0); auto state2 = [&]() { gid2 = timeline->m_groups->getRootId(clips[2]); check_roots(gid1, gid1, gid2, gid2); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]})); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->checkConsistency()); }; INFO("Test 2"); checkJsonParsing(); state2(); auto g3 = std::unordered_set({clips[0], clips[3]}); REQUIRE(timeline->requestClipsGroup(g3, true, GroupType::Normal) > 0); auto state3 = [&]() { REQUIRE(undoStack->index() == init_index + 3); gid3 = timeline->m_groups->getRootId(clips[0]); check_roots(gid3, gid3, gid3, gid3); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal); REQUIRE(timeline->m_groups->getSubtree(gid3) == std::unordered_set({gid1, clips[0], clips[1], gid3, gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid3) == std::unordered_set({clips[2], clips[3], clips[0], clips[1]})); REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]})); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); REQUIRE(timeline->checkConsistency()); }; INFO("Test 3"); checkJsonParsing(); state3(); undoStack->undo(); INFO("Test 4"); checkJsonParsing(); state2(); undoStack->redo(); INFO("Test 5"); checkJsonParsing(); state3(); undoStack->undo(); INFO("Test 6"); checkJsonParsing(); state2(); undoStack->undo(); INFO("Test 8"); checkJsonParsing(); state1(); undoStack->undo(); INFO("Test 9"); checkJsonParsing(); check_roots(clips[0], clips[1], clips[2], clips[3]); undoStack->redo(); INFO("Test 10"); checkJsonParsing(); state1(); undoStack->redo(); INFO("Test 11"); checkJsonParsing(); state2(); REQUIRE(timeline->requestClipsGroup(g3) > 0); checkJsonParsing(); state3(); undoStack->undo(); checkJsonParsing(); state2(); undoStack->undo(); checkJsonParsing(); state1(); undoStack->undo(); checkJsonParsing(); check_roots(clips[0], clips[1], clips[2], clips[3]); } SECTION("Group deletion undo") { CAPTURE(clips[0]); CAPTURE(clips[1]); CAPTURE(clips[2]); CAPTURE(clips[3]); REQUIRE(timeline->requestClipMove(clips[0], tid1, 10)); REQUIRE(timeline->requestClipMove(clips[1], tid3, 10 + length)); REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length)); REQUIRE(timeline->requestClipMove(clips[3], tid3, 50 + 3 * length)); auto state0 = [&]() { REQUIRE(timeline->getTrackById(tid1)->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getTrackClipsCount(tid3) == 2); REQUIRE(timeline->getClipsCount() == 4); REQUIRE(timeline->getClipTrackId(clips[0]) == tid1); REQUIRE(timeline->getClipTrackId(clips[2]) == tid1); REQUIRE(timeline->getClipTrackId(clips[1]) == tid3); REQUIRE(timeline->getClipTrackId(clips[3]) == tid3); REQUIRE(timeline->getClipPosition(clips[0]) == 10); REQUIRE(timeline->getClipPosition(clips[1]) == 10 + length); REQUIRE(timeline->getClipPosition(clips[2]) == 15 + 2 * length); REQUIRE(timeline->getClipPosition(clips[3]) == 50 + 3 * length); REQUIRE(timeline->checkConsistency()); }; auto state = [&](int gid1, int gid2, int gid3) { state0(); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal); REQUIRE(timeline->checkConsistency()); }; state0(); auto g1 = std::unordered_set({clips[0], clips[1]}); int gid1, gid2, gid3; gid1 = timeline->requestClipsGroup(g1, true, GroupType::AVSplit); REQUIRE(gid1 > 0); auto g2 = std::unordered_set({clips[2], clips[3]}); gid2 = timeline->requestClipsGroup(g2, true, GroupType::AVSplit); REQUIRE(gid2 > 0); auto g3 = std::unordered_set({clips[0], clips[3]}); gid3 = timeline->requestClipsGroup(g3, true, GroupType::Normal); REQUIRE(gid3 > 0); state(gid1, gid2, gid3); std::vector all_clips{clips[0], clips[2], clips[1], clips[3]}; for (int i = 0; i < 4; i++) { REQUIRE(timeline->requestItemDeletion(all_clips[i])); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid3) == 0); REQUIRE(timeline->getClipsCount() == 0); REQUIRE(timeline->getTrackById(tid1)->checkConsistency()); REQUIRE(timeline->getTrackById(tid3)->checkConsistency()); REQUIRE(timeline->checkConsistency()); undoStack->undo(); state(gid1, gid2, gid3); undoStack->redo(); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid3) == 0); REQUIRE(timeline->getClipsCount() == 0); REQUIRE(timeline->checkConsistency()); undoStack->undo(); state(gid1, gid2, gid3); } // we undo the three grouping operations undoStack->undo(); state0(); undoStack->undo(); state0(); undoStack->undo(); state0(); } SECTION("Group creation and query from timeline") { REQUIRE(timeline->requestClipMove(clips[0], tid1, 10)); REQUIRE(timeline->requestClipMove(clips[1], tid1, 10 + length)); REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length)); REQUIRE(timeline->requestClipMove(clips[3], tid1, 50 + 3 * length)); auto state1 = [&]() { REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]})); REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]})); REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]})); REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]})); REQUIRE(timeline->checkConsistency()); }; state1(); auto g1 = std::unordered_set({clips[0], clips[3]}); int gid1, gid2, gid3; REQUIRE(timeline->requestClipsGroup(g1) > 0); auto state2 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == g1); REQUIRE(timeline->getGroupElements(clips[3]) == g1); REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]})); REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]})); REQUIRE(timeline->checkConsistency()); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); auto g2 = std::unordered_set({clips[2], clips[1]}); REQUIRE(timeline->requestClipsGroup(g2) > 0); auto state3 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == g1); REQUIRE(timeline->getGroupElements(clips[3]) == g1); REQUIRE(timeline->getGroupElements(clips[2]) == g2); REQUIRE(timeline->getGroupElements(clips[1]) == g2); REQUIRE(timeline->checkConsistency()); }; state3(); undoStack->undo(); state2(); undoStack->redo(); state3(); auto g3 = std::unordered_set({clips[0], clips[1]}); REQUIRE(timeline->requestClipsGroup(g3) > 0); auto all_g = std::unordered_set({clips[0], clips[1], clips[2], clips[3]}); auto state4 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == all_g); REQUIRE(timeline->getGroupElements(clips[3]) == all_g); REQUIRE(timeline->getGroupElements(clips[2]) == all_g); REQUIRE(timeline->getGroupElements(clips[1]) == all_g); REQUIRE(timeline->checkConsistency()); }; state4(); undoStack->undo(); state3(); undoStack->redo(); state4(); REQUIRE(timeline->requestClipUngroup(clips[0])); state3(); undoStack->undo(); state4(); REQUIRE(timeline->requestClipUngroup(clips[1])); state3(); undoStack->undo(); state4(); undoStack->redo(); state3(); REQUIRE(timeline->requestClipUngroup(clips[0])); REQUIRE(timeline->getGroupElements(clips[2]) == g2); REQUIRE(timeline->getGroupElements(clips[1]) == g2); REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]})); REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]})); REQUIRE(timeline->requestClipUngroup(clips[1])); state1(); } } TEST_CASE("Complex Functions", "[GroupsModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); GroupsModel groups(timeline); int init_index = undoStack->index(); SECTION("MergeSingleGroups") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 3); groups.setGroup(2, 4); groups.setGroup(3, 1); groups.setGroup(4, 1); groups.setGroup(5, 0); auto test_tree = [&]() { REQUIRE(groups.getSubtree(1) == std::unordered_set({0, 1, 2, 3, 4, 5})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 4})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.checkConsistency(false)); }; test_tree(); - REQUIRE(groups.mergeSingleGroups(1, undo, redo)); + REQUIRE(groups.mergeSingleGroups(1, undo, redo, list)); auto test_tree2 = [&]() { REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 2, 5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2, 5})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("MergeSingleGroups2") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 3; i++) { groups.createGroupItem(i); } groups.setGroup(1, 0); groups.setGroup(2, 1); auto test_tree = [&]() { REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.checkConsistency(false)); }; test_tree(); - REQUIRE(groups.mergeSingleGroups(0, undo, redo)); + REQUIRE(groups.mergeSingleGroups(0, undo, redo, list)); auto test_tree2 = [&]() { REQUIRE(groups.getSubtree(2) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getRootId(2) == 2); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("MergeSingleGroups3") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 2); groups.setGroup(1, 0); groups.setGroup(3, 1); groups.setGroup(4, 1); groups.setGroup(5, 4); auto test_tree = [&]() { for (int i = 0; i < 6; i++) { REQUIRE(groups.getRootId(i) == 2); } REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({4, 3})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({0})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({5})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.checkConsistency(false)); }; test_tree(); - REQUIRE(groups.mergeSingleGroups(2, undo, redo)); + REQUIRE(groups.mergeSingleGroups(2, undo, redo, list)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.getRootId(3) == 1); REQUIRE(groups.getRootId(5) == 1); REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 3, 5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 5})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("Split leaf") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; auto criterion2 = [](int a) { return a % 2 != 0; }; // We create a leaf groups.createGroupItem(1); auto test_leaf = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.isLeaf(1)); REQUIRE(groups.m_upLink.size() == 1); REQUIRE(groups.checkConsistency()); }; test_leaf(); - REQUIRE(groups.split(1, criterion, undo, redo)); + REQUIRE(groups.split(1, criterion, undo, redo, list)); test_leaf(); undo(); test_leaf(); redo(); - REQUIRE(groups.split(1, criterion2, undo, redo)); + REQUIRE(groups.split(1, criterion2, undo, redo, list)); test_leaf(); undo(); test_leaf(); redo(); } SECTION("Simple split Tree") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; // We create a very simple tree for (int i = 0; i < 3; i++) { groups.createGroupItem(i); } groups.setGroup(1, 0); groups.setGroup(2, 0); auto test_tree = [&]() { REQUIRE(groups.getRootId(0) == 0); REQUIRE(groups.getRootId(1) == 0); REQUIRE(groups.getRootId(2) == 0); REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 2})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.checkConsistency()); }; test_tree(); - REQUIRE(groups.split(0, criterion, undo, redo)); + REQUIRE(groups.split(0, criterion, undo, redo, list)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.getRootId(2) == 2); REQUIRE(groups.getSubtree(2) == std::unordered_set({2})); REQUIRE(groups.getSubtree(1) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("complex split Tree") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 != 0; }; for (int i = 0; i < 9; i++) { groups.createGroupItem(i); } groups.setGroup(0, 3); groups.setGroup(1, 0); groups.setGroup(3, 2); groups.setGroup(4, 3); groups.setGroup(5, 8); groups.setGroup(6, 0); groups.setGroup(7, 8); groups.setGroup(8, 2); auto test_tree = [&]() { for (int i = 0; i < 9; i++) { REQUIRE(groups.getRootId(i) == 2); } REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 6})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({3, 8})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0, 4})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(7) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(8) == std::unordered_set({5, 7})); REQUIRE(groups.checkConsistency()); }; test_tree(); - REQUIRE(groups.split(2, criterion, undo, redo)); + REQUIRE(groups.split(2, criterion, undo, redo, list)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(6) == 3); REQUIRE(groups.getRootId(3) == 3); REQUIRE(groups.getRootId(4) == 3); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({6, 4})); // new tree int newRoot = groups.getRootId(1); REQUIRE(groups.getRootId(1) == newRoot); REQUIRE(groups.getRootId(5) == newRoot); REQUIRE(groups.getRootId(7) == newRoot); int other = -1; REQUIRE(groups.getDirectChildren(newRoot).size() == 2); for (int c : groups.getDirectChildren(newRoot)) if (c != 1) other = c; REQUIRE(other != -1); REQUIRE(groups.getSubtree(newRoot) == std::unordered_set({1, 5, 7, newRoot, other})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(7) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(newRoot) == std::unordered_set({1, other})); REQUIRE(groups.getDirectChildren(other) == std::unordered_set({5, 7})); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("Splitting preserves group type") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; + Updates list; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; // We create a very simple tree for (int i = 0; i <= 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 4); groups.setGroup(2, 4); groups.setGroup(1, 5); groups.setGroup(3, 5); groups.setGroup(4, 6); groups.setGroup(5, 6); groups.setType(4, GroupType::AVSplit); groups.setType(5, GroupType::AVSplit); groups.setType(6, GroupType::Normal); auto test_tree = [&]() { REQUIRE(groups.m_upLink.size() == 7); for (int i = 0; i <= 6; i++) { REQUIRE(groups.getRootId(i) == 6); } REQUIRE(groups.getSubtree(6) == std::unordered_set({0, 1, 2, 3, 4, 5, 6})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({0, 2})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({1, 3})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({4, 5})); REQUIRE(groups.getType(4) == GroupType::AVSplit); REQUIRE(groups.getType(5) == GroupType::AVSplit); REQUIRE(groups.getType(6) == GroupType::Normal); REQUIRE(groups.checkConsistency()); }; test_tree(); qDebug() << " done testing"; - REQUIRE(groups.split(6, criterion, undo, redo)); + REQUIRE(groups.split(6, criterion, undo, redo, list)); qDebug() << " done spliting"; auto test_tree2 = [&]() { // REQUIRE(groups.m_upLink.size() == 6); int r1 = groups.getRootId(0); int r2 = groups.getRootId(1); bool ok = r1 == 4 || r2 == 5; REQUIRE(ok); REQUIRE(groups.getRootId(2) == r1); REQUIRE(groups.getRootId(3) == r2); REQUIRE(groups.getSubtree(r1) == std::unordered_set({r1, 0, 2})); REQUIRE(groups.getSubtree(r2) == std::unordered_set({r2, 1, 3})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(r1) == std::unordered_set({0, 2})); REQUIRE(groups.getDirectChildren(r2) == std::unordered_set({1, 3})); REQUIRE(groups.getType(r1) == GroupType::AVSplit); REQUIRE(groups.getType(r2) == GroupType::AVSplit); REQUIRE(groups.checkConsistency()); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); undo(); test_tree(); redo(); test_tree2(); } } diff --git a/tests/modeltest.cpp b/tests/modeltest.cpp index 8db1edf90..c115e7fb7 100644 --- a/tests/modeltest.cpp +++ b/tests/modeltest.cpp @@ -1,2117 +1,2125 @@ #include "test_utils.hpp" using namespace fakeit; std::default_random_engine g(42); Mlt::Profile profile_model; TEST_CASE("Basic creation/deletion of a track", "[TrackModel]") { auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); int id1 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->getTrackPosition(id1) == 0); // In the current implementation, when a track is added/removed, the model is notified with _resetView Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id2 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); REQUIRE(timeline->getTrackPosition(id2) == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id3 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); REQUIRE(timeline->getTrackPosition(id3) == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id4; REQUIRE(timeline->requestTrackInsertion(1, id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 4); REQUIRE(timeline->getTrackPosition(id1) == 0); REQUIRE(timeline->getTrackPosition(id4) == 1); REQUIRE(timeline->getTrackPosition(id2) == 2); REQUIRE(timeline->getTrackPosition(id3) == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); // Test deletion REQUIRE(timeline->requestTrackDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 0); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); SECTION("Delete a track with groups") { int tid1, tid2; REQUIRE(timeline->requestTrackInsertion(-1, tid1)); REQUIRE(timeline->requestTrackInsertion(-1, tid2)); REQUIRE(timeline->checkConsistency()); QString binId = createProducer(profile_model, "red", binModel); int length = 20; int cid1, cid2, cid3, cid4; REQUIRE(timeline->requestClipInsertion(binId, tid1, 2, cid1)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 0, cid2)); REQUIRE(timeline->requestClipInsertion(binId, tid2, length, cid3)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 2 * length, cid4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 4); REQUIRE(timeline->getTracksCount() == 2); auto g1 = std::unordered_set({cid1, cid3}); auto g2 = std::unordered_set({cid2, cid4}); auto g3 = std::unordered_set({cid1, cid4}); REQUIRE(timeline->requestClipsGroup(g1)); REQUIRE(timeline->requestClipsGroup(g2)); REQUIRE(timeline->requestClipsGroup(g3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->checkConsistency()); } } TEST_CASE("Basic creation/deletion of a clip", "[ClipModel]") { auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "green", binModel); REQUIRE(timeline->getClipsCount() == 0); int id1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->checkConsistency()); int id2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->checkConsistency()); int id3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->checkConsistency()); // Test deletion REQUIRE(timeline->requestItemDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->requestItemDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->requestItemDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 0); } TEST_CASE("Clip manipulation", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); Fake(Method(timMock, _beginInsertRows)); Fake(Method(timMock, _beginRemoveRows)); Fake(Method(timMock, _endInsertRows)); Fake(Method(timMock, _endRemoveRows)); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducer(profile_model, "green", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); Verify(Method(timMock, _resetView)).Exactly(3_Times); RESET(timMock); // for testing purposes, we make sure the clip will behave as regular clips // (ie their size is fixed, we cannot resize them past their original size) timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; SECTION("Insert a clip in a track and change track") { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(timeline->getClipPosition(cid1) == -1); int pos = 10; REQUIRE(timeline->requestClipMove(cid1, tid1, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); // Check that the model was correctly notified CHECK_INSERT(Once); pos = 1; REQUIRE(timeline->requestClipMove(cid1, tid2, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); CHECK_MOVE(Once); // Check conflicts int pos2 = binModel->getClipByBinID(binId)->frameDuration(); REQUIRE(timeline->requestClipMove(cid2, tid1, pos2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 + 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 - 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); } int length = binModel->getClipByBinID(binId)->frameDuration(); SECTION("Insert consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == length); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_INSERT(Once); } SECTION("Resize orphan clip") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); auto inOut = std::pair{0, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 10, false) == -1); REQUIRE(timeline->requestItemResize(cid2, length + 1, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 2, false) == 2); REQUIRE(timeline->checkConsistency()); inOut = std::pair{3, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 2); REQUIRE(timeline->requestItemResize(cid2, length, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 2); CAPTURE(timeline->m_allClips[cid2]->m_producer->get_in()); REQUIRE(timeline->requestItemResize(cid2, length - 2, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestItemResize(cid2, length - 3, true) == length - 3); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == length - 3); } SECTION("Resize inserted clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == 5); REQUIRE(timeline->getClipPosition(cid1) == 0); CHECK_RESIZE(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 6, true) == -1); REQUIRE(timeline->requestItemResize(cid1, 6, false) == -1); REQUIRE(timeline->checkConsistency()); NO_OTHERS(); REQUIRE(timeline->requestItemResize(cid2, length - 5, false) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPosition(cid2) == 10); CHECK_RESIZE(Once); REQUIRE(timeline->requestItemResize(cid1, 10, true) == 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_RESIZE(Once); } SECTION("Change track of resized clips") { // // REQUIRE(timeline->allowClipMove(cid2, tid1, 5)); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); // // REQUIRE(timeline->allowClipMove(cid1, tid2, 10)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->requestItemResize(cid1, 5, false) == 5); REQUIRE(timeline->checkConsistency()); // // REQUIRE(timeline->allowClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); } SECTION("Clip Move") { REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid1, tid1, 5 + length)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 5); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 3 + length)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state(); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 0); }; state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, length - 5)); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestClipMove(cid1, tid1, length - 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); } SECTION("Move and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestItemResize(cid1, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 2); }; state(); // try to resize past the left end REQUIRE(timeline->requestItemResize(cid1, length, false) == -1); state(); REQUIRE(timeline->requestItemResize(cid1, length - 4, true) == length - 4); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); REQUIRE(timeline->requestItemResize(cid2, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4 + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state2(); // the gap between the two clips is 1 frame, we try to resize them by 2 frames REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state2(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state2(); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state3(); // Now the gap is 0 frames, the resize should still fail REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state3(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state3(); // We move cid1 out of the way REQUIRE(timeline->requestClipMove(cid1, tid2, 0)); // now resize should work REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == length - 2); REQUIRE(timeline->requestItemResize(cid2, length, false) == length); REQUIRE(timeline->checkConsistency()); } SECTION("Group move") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, length + 3)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 5)); REQUIRE(timeline->requestClipMove(cid4, tid2, 4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid4, tid2, 9)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); }; state(); // grouping REQUIRE(timeline->requestClipsGroup({cid1, cid3})); REQUIRE(timeline->requestClipsGroup({cid1, cid4})); // move left is now forbidden, because clip1 is at position 0 REQUIRE_FALSE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); state(); // this move is impossible, because clip1 runs into clip2 REQUIRE_FALSE(timeline->requestClipMove(cid4, tid2, 9)); state(); // this move is possible REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 8)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid3) == 0); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 3); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 8); REQUIRE(timeline->getClipPosition(cid4) == 7); }; state1(); // this move is possible REQUIRE(timeline->requestClipMove(cid1, tid2, 8)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 2); REQUIRE(timeline->getTrackClipsCount(tid3) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid2); REQUIRE(timeline->getClipTrackId(cid4) == tid3); REQUIRE(timeline->getClipPosition(cid1) == 8); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5 + 8); REQUIRE(timeline->getClipPosition(cid4) == 4 + 8); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, 3)); state1(); } SECTION("Group move consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 7)); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + length)); REQUIRE(timeline->requestClipMove(cid3, tid1, 7 + 2 * length)); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 3 * length)); REQUIRE(timeline->requestClipsGroup({cid1, cid2, cid3, cid4})); auto state = [&](int tid, int start) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid) == 4); int i = 0; for (int cid : std::vector({cid1, cid2, cid3, cid4})) { REQUIRE(timeline->getClipTrackId(cid) == tid); REQUIRE(timeline->getClipPosition(cid) == start + i * length); REQUIRE(timeline->getClipPlaytime(cid) == length); i++; } }; state(tid1, 7); auto check_undo = [&](int target, int tid, int oldTid) { state(tid, target); undoStack->undo(); state(oldTid, 7); undoStack->redo(); state(tid, target); undoStack->undo(); state(oldTid, 7); }; REQUIRE(timeline->requestClipMove(cid1, tid1, 6)); qDebug() << "state1"; state(tid1, 6); undoStack->undo(); state(tid1, 7); undoStack->redo(); state(tid1, 6); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); qDebug() << "state2"; state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->redo(); state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->undo(); state(tid1, 7); REQUIRE(timeline->requestClipMove(cid3, tid1, 1 + 2 * length)); qDebug() << "state3"; check_undo(1, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 4 + 3 * length)); qDebug() << "state4"; check_undo(4, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 11 + 3 * length)); qDebug() << "state5"; check_undo(11, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 13 + length)); qDebug() << "state6"; check_undo(13, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 20)); qDebug() << "state7"; check_undo(20, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 4 * length)); qDebug() << "state8"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + 2 * length)); qDebug() << "state9"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 7 + length)); qDebug() << "state10"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid2, 8 + length)); qDebug() << "state11"; check_undo(8, tid2, tid1); } SECTION("Group move to unavailable track") { REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); REQUIRE(timeline->requestClipMove(cid2, tid2, 12)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 12); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 10)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 100)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid3, 100)); state(); } SECTION("Group move with non-consecutive track ids") { int tid5 = TrackModel::construct(timeline); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid6 = TrackModel::construct(timeline); REQUIRE(tid5 + 1 != tid6); REQUIRE(timeline->requestClipMove(cid1, tid5, 10)); REQUIRE(timeline->requestClipMove(cid2, tid5, length + 10)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&](int t) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(t) == 2); REQUIRE(timeline->getClipTrackId(cid1) == t); REQUIRE(timeline->getClipTrackId(cid2) == t); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 10 + length); }; state(tid5); REQUIRE(timeline->requestClipMove(cid1, tid6, 10)); state(tid6); } SECTION("Movement of AV groups") { int tid6b = TrackModel::construct(timeline, -1, -1, QString(), true); int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true); int tid5 = TrackModel::construct(timeline); int tid5b = TrackModel::construct(timeline); QString binId3 = createProducerWithSound(profile_model, binModel); int cid6 = -1; REQUIRE(timeline->requestClipInsertion(binId3, tid5, 3, cid6, true, true, false)); int cid7 = timeline->m_groups->getSplitPartner(cid6); auto state = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5); REQUIRE(timeline->getClipTrackId(cid7) == tid6); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); }; state(3); // simple translation on the right REQUIRE(timeline->requestClipMove(cid6, tid5, 10, true, true, true)); state(10); undoStack->undo(); state(3); undoStack->redo(); state(10); // simple translation on the left, moving the audio clip this time REQUIRE(timeline->requestClipMove(cid7, tid6, 1, true, true, true)); state(1); undoStack->undo(); state(10); undoStack->redo(); state(1); // change track, moving video REQUIRE(timeline->requestClipMove(cid6, tid5b, 7, true, true, true)); auto state2 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5b) == 1); REQUIRE(timeline->getTrackClipsCount(tid6b) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5b); REQUIRE(timeline->getClipTrackId(cid7) == tid6b); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); }; state2(7); undoStack->undo(); state(1); undoStack->redo(); state2(7); // change track, moving audio REQUIRE(timeline->requestClipMove(cid7, tid6b, 2, true, true, true)); state2(2); undoStack->undo(); state2(7); undoStack->redo(); state2(2); undoStack->undo(); undoStack->undo(); state(1); // Switching audio and video, going to the extra track REQUIRE(timeline->requestClipMove(cid7, tid5b, 2, true, true, true)); auto state3 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5b) == 1); REQUIRE(timeline->getTrackClipsCount(tid6b) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid6b); REQUIRE(timeline->getClipTrackId(cid7) == tid5b); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly); }; state3(2); undoStack->undo(); state(1); undoStack->redo(); state3(2); undoStack->undo(); state(1); // Switching audio and video, switching tracks in place REQUIRE(timeline->requestClipMove(cid6, tid6, 1, true, true, true)); auto state4 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid6); REQUIRE(timeline->getClipTrackId(cid7) == tid5); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly); }; state4(1); undoStack->undo(); state(1); undoStack->redo(); state4(1); REQUIRE(false); } SECTION("Clip copy") { int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int l = timeline->getClipPlaytime(cid6); REQUIRE(timeline->requestItemResize(cid6, l - 3, true, true, -1) == l - 3); REQUIRE(timeline->requestItemResize(cid6, l - 7, false, true, -1) == l - 7); int newId; std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo)); + Updates list; + REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo, list)); REQUIRE(timeline->m_allClips[cid6]->binId() == timeline->m_allClips[newId]->binId()); // TODO check effects } } TEST_CASE("Check id unicity", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); std::vector track_ids; std::unordered_set all_ids; std::bernoulli_distribution coin(0.5); const int nbr = 20; for (int i = 0; i < nbr; i++) { if (coin(g)) { int tid = TrackModel::construct(timeline); REQUIRE(all_ids.count(tid) == 0); all_ids.insert(tid); track_ids.push_back(tid); REQUIRE(timeline->getTracksCount() == track_ids.size()); } else { int cid = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(all_ids.count(cid) == 0); all_ids.insert(cid); REQUIRE(timeline->getClipsCount() == all_ids.size() - track_ids.size()); } } REQUIRE(timeline->checkConsistency()); REQUIRE(all_ids.size() == nbr); REQUIRE(all_ids.size() != track_ids.size()); } TEST_CASE("Undo and Redo", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; int length = 20; int nclips = timeline->m_allClips.size(); SECTION("requestCreateClip") { // an invalid clip id shouln't get created { int temp; Fun undo = []() { return true; }; Fun redo = []() { return true; }; - REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::VideoOnly, undo, redo)); + Updates list; + REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::VideoOnly, undo, redo, list)); } auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; - REQUIRE(timeline->requestClipCreation(binId3, cid3, PlaylistState::VideoOnly, undo, redo)); + Updates list; + REQUIRE(timeline->requestClipCreation(binId3, cid3, PlaylistState::VideoOnly, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; - REQUIRE(timeline->requestClipCreation(binId4, cid4, PlaylistState::VideoOnly, undo, redo)); + Updates list; + REQUIRE(timeline->requestClipCreation(binId4, cid4, PlaylistState::VideoOnly, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == -1); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } SECTION("requestInsertClip") { auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; REQUIRE(timeline->requestClipInsertion(binId3, tid1, 12, cid3, true)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; REQUIRE(timeline->requestClipInsertion(binId4, tid2, 17, cid4, true)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid4) == 17); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } int init_index = undoStack->index(); SECTION("Basic move undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(Once); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(Once); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(Once); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 2 * length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(Once); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(Once); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(Once); undoStack->undo(); CHECK_MOVE(Once); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(undoStack->index() == init_index); CHECK_REMOVE(Once); } SECTION("Basic resize orphan clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->redo(); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->undo(); REQUIRE(undoStack->index() == init_index); REQUIRE(timeline->getClipPlaytime(cid2) == length); } SECTION("Basic resize inserted clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); auto check = [&](int pos, int l) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPlaytime(cid2) == l); REQUIRE(timeline->getClipPosition(cid2) == pos); }; REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); INFO("Test 1"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); INFO("Test 2"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); INFO("Test 3"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); INFO("Test 4"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 5"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->redo(); INFO("Test 6"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 7"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->undo(); INFO("Test 8"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); } SECTION("Clip Insertion Undo") { QString binId3 = createProducer(profile_model, "red", binModel); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int cid3; REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 5, cid3)); state1(); REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 6, cid3)); state1(); REQUIRE(timeline->requestClipInsertion(binId3, tid1, 5 + length, cid3)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 5 + length); REQUIRE(timeline->m_allClips[cid3]->isValid()); REQUIRE(undoStack->index() == init_index + 2); }; state2(); REQUIRE(timeline->requestClipMove(cid3, tid1, 10 + length)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 3); }; state3(); REQUIRE(timeline->requestItemResize(cid3, 1, true) == 1); auto state4 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPlaytime(cid3) == 1); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 4); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); } SECTION("Clip Deletion undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int nbClips = timeline->getClipsCount(); REQUIRE(timeline->requestItemDeletion(cid1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipsCount() == nbClips - 1); REQUIRE(undoStack->index() == init_index + 2); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->undo(); state1(); } SECTION("Track insertion undo") { int nb_tracks = timeline->getTracksCount(); std::map orig_trackPositions, final_trackPositions; for (const auto &it : timeline->m_iteratorTable) { int track = it.first; int pos = timeline->getTrackPosition(track); orig_trackPositions[track] = pos; if (pos >= 1) pos++; final_trackPositions[track] = pos; } auto checkPositions = [&](const std::map &pos) { for (const auto &p : pos) { REQUIRE(timeline->getTrackPosition(p.first) == p.second); } }; checkPositions(orig_trackPositions); int new_tid; REQUIRE(timeline->requestTrackInsertion(1, new_tid)); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); undoStack->redo(); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); } SECTION("Track deletion undo") { int nb_clips = timeline->getClipsCount(); int nb_tracks = timeline->getTracksCount(); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipsCount() == nb_clips); REQUIRE(timeline->getTracksCount() == nb_tracks); }; state1(); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); undoStack->redo(); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); } int clipCount = timeline->m_allClips.size(); SECTION("Clip creation and resize") { int cid6; auto state0 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount); REQUIRE(timeline->checkConsistency()); }; state0(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(timeline->requestClipCreation(binId, cid6, PlaylistState::VideoOnly, undo, redo)); + Updates list; + REQUIRE(timeline->requestClipCreation(binId, cid6, PlaylistState::VideoOnly, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } int l = timeline->getClipPlaytime(cid6); auto state1 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l); }; state1(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(timeline->requestItemResize(cid6, l - 5, true, true, undo, redo, false)); + Updates list; + REQUIRE(timeline->requestItemResize(cid6, l - 5, true, true, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state2(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(timeline->requestClipMove(cid6, tid1, 7, true, true, undo, redo)); + Updates list; + REQUIRE(timeline->requestClipMove(cid6, tid1, 7, true, true, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } auto state3 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 7); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state3(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(timeline->requestItemResize(cid6, l - 6, false, true, undo, redo, false)); + Updates list; + REQUIRE(timeline->requestItemResize(cid6, l - 6, false, true, undo, redo, list)); pCore->pushUndo(undo, redo, QString()); } auto state4 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 8); REQUIRE(timeline->getClipPlaytime(cid6) == l - 6); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); } } TEST_CASE("Snapping", "[Snapping]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel, 50); QString binId2 = createProducer(profile_model, "blue", binModel); int tid1 = TrackModel::construct(timeline); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; int length = timeline->getClipPlaytime(cid1); int length2 = timeline->getClipPlaytime(cid2); SECTION("getBlankSizeNearClip") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 25 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 15); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 15); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 10 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 0); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); } SECTION("Snap move to a single clip") { int beg = 30; // in the absence of other clips, a valid move shouldn't be modified for (int snap = -1; snap <= 5; ++snap) { REQUIRE(timeline->suggestClipMove(cid2, tid2, beg, snap) == beg); REQUIRE(timeline->suggestClipMove(cid2, tid2, beg + length, snap) == beg + length); REQUIRE(timeline->checkConsistency()); } // We add a clip in first track to create snap points REQUIRE(timeline->requestClipMove(cid1, tid1, beg)); // Now a clip in second track should snap to beginning auto check_snap = [&](int pos, int perturb, int snap) { if (snap >= perturb) { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, snap) == pos); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, snap) == pos); } else { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, snap) == pos + perturb); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, snap) == pos - perturb); } }; for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { // snap to beginning check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); // snap to end check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); } } // Same test, but now clip is moved in position 0 first REQUIRE(timeline->requestClipMove(cid2, tid2, 0)); for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPosition(cid2) == 0); } } } } TEST_CASE("Advanced trimming operations", "[Trimming]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducerWithSound(profile_model, binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid5 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid7 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int audio1 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio2 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; timeline->m_allClips[cid5]->m_endlessResize = false; timeline->m_allClips[cid6]->m_endlessResize = false; timeline->m_allClips[cid7]->m_endlessResize = false; SECTION("Clip splitting") { // Trivial split REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestItemResize(cid2, l - 3, true) == l - 3); REQUIRE(timeline->requestItemResize(cid2, l - 5, false) == l - 5); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, l + l - 5)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == l - 5); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == l - 4); }; state(); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l + l - 5)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 9); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == 5); REQUIRE(timeline->getClipPtr(splitted)->getIn() == 6); REQUIRE(timeline->getClipPtr(splitted)->getOut() == l - 4); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } SECTION("Split and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); int l = timeline->getClipPlaytime(cid1); timeline->m_allClips[cid1]->m_endlessResize = false; auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); }; state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid1, 9)); int splitted = timeline->getClipByPosition(tid1, 10); timeline->m_allClips[splitted]->m_endlessResize = false; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 9); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid2, 9, true, true)); REQUIRE(timeline->requestItemResize(splitted, l - 3, true, true) == -1); REQUIRE(timeline->requestItemResize(splitted, l, false, true) == l); REQUIRE(timeline->requestItemResize(cid1, 5, false, true) == -1); REQUIRE(timeline->requestItemResize(cid1, l, true, true) == l); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid2); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(splitted) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 5); }; state3(); undoStack->undo(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Clip splitting 2") { // More complex group structure split split int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * l)); REQUIRE(timeline->requestClipMove(cid4, tid2, 0)); REQUIRE(timeline->requestClipMove(cid5, tid2, l)); REQUIRE(timeline->requestClipMove(cid6, tid2, 2 * l)); REQUIRE(timeline->requestClipMove(cid7, tid1, 200)); int gid1 = timeline->requestClipsGroup(std::unordered_set({cid1, cid4}), true, GroupType::Normal); int gid2 = timeline->requestClipsGroup(std::unordered_set({cid2, cid5}), true, GroupType::Normal); int gid3 = timeline->requestClipsGroup(std::unordered_set({cid3, cid6}), true, GroupType::Normal); int gid4 = timeline->requestClipsGroup(std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7}), true, GroupType::Normal); auto state = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->m_groups->getDirectChildren(gid1) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid1, gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7})); }; state(); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 2 * l)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); int splitted2 = timeline->getClipByPosition(tid2, l + 5); REQUIRE(splitted != splitted2); auto check_groups = [&]() { REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({splitted, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({splitted, splitted2, cid3, cid6, cid7})); int g1b = timeline->m_groups->m_upLink[cid1]; int g2b = timeline->m_groups->m_upLink[cid2]; int g4b = timeline->m_groups->getRootId(cid1); REQUIRE(timeline->m_groups->getDirectChildren(g1b) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(g2b) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(g4b) == std::unordered_set({g1b, g2b})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid4, cid5})); }; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid1, l + 4 + 10, true, true)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10, true, true)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid3 ? tid1 : tid2)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid6 ? tid2 : tid3)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 210); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state3(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Simple audio split") { int l = timeline->getClipPlaytime(audio1); REQUIRE(timeline->requestClipMove(audio1, tid1, 3)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1})); }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2)); int splitted1 = timeline->getClipByPosition(tid2, 3); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPosition(splitted1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid2); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1})); int g1 = timeline->m_groups->getDirectAncestor(audio1); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); // We also make sure that clips that are audio only cannot be further splitted REQUIRE(timeline->requestClipMove(cid1, tid1, 30)); // This is a color clip, shouldn't be splittable REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2)); REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, splitted1, tid2)); } SECTION("Split audio on a selection") { int l = timeline->getClipPlaytime(audio2); REQUIRE(timeline->requestClipMove(audio1, tid1, 0)); REQUIRE(timeline->requestClipMove(audio2, tid1, l)); REQUIRE(timeline->requestClipMove(audio3, tid1, 2 * l)); std::unordered_set selection{audio1, audio3, audio2}; REQUIRE(timeline->requestClipsGroup(selection, false, GroupType::Selection)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, audio2, audio3})); int sel = timeline->m_temporarySelectionGroup; // check that selection is preserved REQUIRE(sel != -1); REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2)); int splitted1 = timeline->getClipByPosition(tid2, 0); int splitted2 = timeline->getClipByPosition(tid2, l); int splitted3 = timeline->getClipByPosition(tid2, 2 * l); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPlaytime(splitted2) == l); REQUIRE(timeline->getClipPlaytime(splitted3) == l); REQUIRE(timeline->getClipPosition(splitted1) == 0); REQUIRE(timeline->getClipPosition(splitted2) == l); REQUIRE(timeline->getClipPosition(splitted3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid2); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); REQUIRE(timeline->getClipTrackId(splitted3) == tid2); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 3); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3})); int sel = timeline->m_temporarySelectionGroup; // check that selection is preserved REQUIRE(sel != -1); REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); REQUIRE(timeline->m_groups->getRootId(audio1) == sel); REQUIRE(timeline->m_groups->getDirectChildren(sel).size() == 3); REQUIRE(timeline->m_groups->getLeaves(sel).size() == 6); int g1 = timeline->m_groups->getDirectAncestor(audio1); int g2 = timeline->m_groups->getDirectAncestor(audio2); int g3 = timeline->m_groups->getDirectAncestor(audio3); REQUIRE(timeline->m_groups->getDirectChildren(sel) == std::unordered_set({g1, g2, g3})); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({audio2, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set({audio3, splitted3})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } } diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 9b3bf7af4..4b51cb8a1 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -1,85 +1,86 @@ #pragma once #include "bin/model/markerlistmodel.hpp" #include "catch.hpp" #include "doc/docundostack.hpp" #include #include #include #include #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" 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 &))); \ + VerifyNoOtherInvocations(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, int))); \ 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); \ + Verify(OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, int)) + \ + OverloadedMethod(timMock, notifyChange, void(const QModelIndex &, const QModelIndex &, const QVector &))) \ + .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); diff --git a/tests/timewarptest.cpp b/tests/timewarptest.cpp index 0ce8a0692..291e0a912 100644 --- a/tests/timewarptest.cpp +++ b/tests/timewarptest.cpp @@ -1,107 +1,109 @@ #include "test_utils.hpp" using namespace fakeit; Mlt::Profile profile_timewarp; TEST_CASE("Test of timewarping", "[Timewarp]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_timewarp, "red", binModel); QString binId2 = createProducer(profile_timewarp, "blue", binModel); QString binId3 = createProducerWithSound(profile_timewarp, binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; SECTION("Timewarping orphan clip") { int originalDuration = timeline->getClipPlaytime(cid3); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid3) == -1); REQUIRE(timeline->getClipSpeed(cid3) == 1.); std::function undo = []() { return true; }; std::function redo = []() { return true; }; + Updates list; - REQUIRE(timeline->requestClipTimeWarp(cid3, 0.1, undo, redo)); + REQUIRE(timeline->requestClipTimeWarp(cid3, 0.1, undo, redo, list)); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 0.1); INFO(timeline->m_allClips[cid3]->getIn()); INFO(timeline->m_allClips[cid3]->getOut()); REQUIRE(timeline->getClipPlaytime(cid3) == originalDuration / 0.1); undo(); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 1.); REQUIRE(timeline->getClipPlaytime(cid3) == originalDuration); redo(); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 0.1); REQUIRE(timeline->getClipPlaytime(cid3) == originalDuration / 0.1); std::function undo2 = []() { return true; }; std::function redo2 = []() { return true; }; - REQUIRE(timeline->requestClipTimeWarp(cid3, 1.2, undo2, redo2)); + Updates list2; + REQUIRE(timeline->requestClipTimeWarp(cid3, 1.2, undo2, redo2, list2)); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 1.2); REQUIRE(timeline->getClipPlaytime(cid3) == int(originalDuration / 1.2)); undo2(); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 0.1); REQUIRE(timeline->getClipPlaytime(cid3) == originalDuration / 0.1); undo(); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == 1.); REQUIRE(timeline->getClipPlaytime(cid3) == originalDuration); // Finally, we test that setting a very high speed isn't possible. // Specifically, it must be impossible to make the clip shorter than one frame int curLength = timeline->getClipPlaytime(cid3); // This is the limit, should work - REQUIRE(timeline->requestClipTimeWarp(cid3, double(curLength), undo2, redo2)); + REQUIRE(timeline->requestClipTimeWarp(cid3, double(curLength), undo2, redo2, list2)); CHECK_UPDATE(TimelineModel::SpeedRole); REQUIRE(timeline->getClipSpeed(cid3) == double(curLength)); REQUIRE(timeline->getClipPlaytime(cid3) == 1); // This is the higher than the limit, should not work - REQUIRE_FALSE(timeline->requestClipTimeWarp(cid3, double(curLength) + 0.1, undo2, redo2)); + REQUIRE_FALSE(timeline->requestClipTimeWarp(cid3, double(curLength) + 0.1, undo2, redo2, list2)); } }