diff --git a/CMakeLists.txt b/CMakeLists.txt index 749acb89d..a24a0f04c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,155 +1,155 @@ project(Kdenlive) # An odd patch version number means development version, while an even one means # stable release. An additional number can be used for bugfix-only releases. # KDE Application Version, managed by release script set (KDE_APPLICATIONS_VERSION_MAJOR "17") set (KDE_APPLICATIONS_VERSION_MINOR "11") set (KDE_APPLICATIONS_VERSION_MICRO "70") set(KDENLIVE_VERSION ${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}) cmake_minimum_required(VERSION 3.0) if(POLICY CMP0063) cmake_policy(SET CMP0063 NEW) endif() if (POLICY CMP0053) cmake_policy(SET CMP0053 NEW) endif() # Minimum versions of main dependencies. set(MLT_MIN_MAJOR_VERSION 6) set(MLT_MIN_MINOR_VERSION 4) set(MLT_MIN_PATCH_VERSION 0) set(MLT_MIN_VERSION ${MLT_MIN_MAJOR_VERSION}.${MLT_MIN_MINOR_VERSION}.${MLT_MIN_PATCH_VERSION}) set(QT_MIN_VERSION 5.6.0) find_package(ECM 5.18.0 REQUIRED CONFIG) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules) include(KDECompilerSettings NO_POLICY_SCOPE) include(FeatureSummary) include(ECMInstallIcons) include(GenerateExportHeader) include(KDEInstallDirs) include(KDECMakeSettings) include(ECMOptionalAddSubdirectory) include(ECMMarkNonGuiExecutable) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) add_definitions(-DQT_NO_CAST_TO_ASCII) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_definitions(-DTRANSLATION_DOMAIN=\"kdenlive\") set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") #SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -fsanitize=address -fno-omit-frame-pointer") # To be switched on when releasing. option(RELEASE_BUILD "Remove Git revision from program version (use for stable releases)" ON) option(BUILD_TESTS "Build tests" ON) # Get current version. set(KDENLIVE_VERSION_STRING "${KDENLIVE_VERSION}") if(NOT RELEASE_BUILD AND EXISTS ${CMAKE_SOURCE_DIR}/.git) # Probably a Git workspace; determine the revision. find_package(Git QUIET) if(GIT_FOUND) exec_program(${GIT_EXECUTABLE} ${CMAKE_SOURCE_DIR} ARGS "log -n 1 --pretty=format:\"%h\"" OUTPUT_VARIABLE KDENLIVE_GIT_REVISION RETURN_VALUE TAG_RESULT ) # git log failed; maybe the repository was checked with depth=1. if(NOT ${TAG_RESULT} EQUAL 0) exec_program(${GIT_EXECUTABLE} ${CMAKE_SOURCE_DIR} ARGS "describe --always" OUTPUT_VARIABLE KDENLIVE_GIT_REVISION ) endif() message(STATUS "Kdenlive Git revision: ${KDENLIVE_GIT_REVISION}") set(KDENLIVE_VERSION_STRING "${KDENLIVE_VERSION} (rev. ${KDENLIVE_GIT_REVISION})") else() message(STATUS "Kdenlive Git revision could not be determined") endif() endif() include(CheckIncludeFiles) check_include_files(malloc.h HAVE_MALLOC_H) check_include_files(pthread.h HAVE_PTHREAD_H) find_package(Qt5 REQUIRED COMPONENTS Core DBus Widgets Svg Quick ) find_package(Qt5 OPTIONAL_COMPONENTS WebKitWidgets QUIET) find_package(KF5 5.23.0 OPTIONAL_COMPONENTS XmlGui QUIET) if (KF5XmlGui_FOUND) message(STATUS "Found KF5 >= 5.23.0 enabling icon coloring") else() message(STATUS "KF5 < 5.23.0 Disable icon coloring") set(KF5_ICON_COMPATIBILITY TRUE) endif() find_package(KF5 REQUIRED COMPONENTS Archive Bookmarks CoreAddons Config ConfigWidgets DBusAddons KIO WidgetsAddons NotifyConfig NewStuff XmlGui Notifications GuiAddons TextWidgets IconThemes Crash Declarative Solid OPTIONAL_COMPONENTS DocTools FileMetaData) if (KF5FileMetaData_FOUND) message(STATUS "Found KF5 FileMetadata to extract file metadata") set(KF5_FILEMETADATA TRUE) else() message(STATUS "KF5 FileMetadata not found, file metadata will not be available") endif() # Search MLT package. find_package(MLT ${MLT_MIN_VERSION} REQUIRED) set_package_properties(MLT PROPERTIES DESCRIPTION "Multimedia framework and video playout server for TV broadcasting" URL "http://mltframework.org" TYPE RUNTIME PURPOSE "Required to do video processing") set(MLT_PREFIX ${MLT_ROOT_DIR}) add_subdirectory(data) if(KF5DocTools_FOUND) add_subdirectory(doc) endif() #add_subdirectory(plugins) ecm_optional_add_subdirectory(po) add_subdirectory(renderer) add_subdirectory(src) add_subdirectory(thumbnailer) ############################ # Tests ############################ if (BUILD_TESTS) message(STATUS "Building TESTS") add_subdirectory(tests) - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -fexceptions") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Wall -fexceptions") include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/src ${MLT_INCLUDE_DIR} ${MLTPP_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/lib/external ${CMAKE_CURRENT_SOURCE_DIR}/lib src ) ADD_EXECUTABLE(runTests ${Tests_SRCS}) target_link_libraries(runTests kdenliveLib) ADD_TEST(runTests runTests -d yes) else() message(STATUS "Not building TESTS") endif() #add_subdirectory(testingArea) install( FILES kdenlive.categories DESTINATION ${KDE_INSTALL_CONFDIR} ) configure_file(config-kdenlive.h.cmake config-kdenlive.h @ONLY) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2b099ce13..628cca6ea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,336 +1,338 @@ 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_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -pedantic -Wextra") +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) #add_definitions( -DQT_NO_CAST_FROM_ASCII ) #add_definitions( -DQT_NO_CAST_TO_ASCII ) 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/timeline_ui.ui ui/timelinebuttons_ui.ui ui/titlewidget_ui.ui ui/trackheader_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) add_library(kdenliveLib STATIC ${kdenlive_SRCS} ${kdenlive_UIS}) 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::Crash KF5::Solid Qt5::Svg ${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() qt5_use_modules( kdenliveLib Widgets Concurrent Qml Quick QuickWidgets) 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() install(TARGETS kdenlive DESTINATION ${BIN_INSTALL_DIR}) install(FILES kdenliveui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/kdenlive) diff --git a/src/abstractmodel/treeitem.cpp b/src/abstractmodel/treeitem.cpp index 98e69ba6f..bfab19586 100644 --- a/src/abstractmodel/treeitem.cpp +++ b/src/abstractmodel/treeitem.cpp @@ -1,267 +1,267 @@ /*************************************************************************** * 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 "treeitem.hpp" #include "abstracttreemodel.hpp" #include #include #include TreeItem::TreeItem(const QList &data, const std::shared_ptr &model, bool isRoot, int id) : m_itemData(data) , m_model(model) , m_depth(0) , m_id(id == -1 ? AbstractTreeModel::getNextId() : id) , m_isInModel(false) , m_isRoot(isRoot) { } std::shared_ptr TreeItem::construct(const QList &data, std::shared_ptr model, bool isRoot, int id) { std::shared_ptr self(new TreeItem(data, std::move(model), isRoot, id)); baseFinishConstruct(self); return self; } // static void TreeItem::baseFinishConstruct(const std::shared_ptr &self) { if (self->m_isRoot) { registerSelf(self); } } TreeItem::~TreeItem() { deregisterSelf(); } std::shared_ptr TreeItem::appendChild(const QList &data) { if (auto ptr = m_model.lock()) { auto child = construct(data, ptr, false); appendChild(child); return child; } qDebug() << "ERROR: Something went wrong when appending child in TreeItem. Model is not available anymore"; Q_ASSERT(false); return std::shared_ptr(); } bool TreeItem::appendChild(std::shared_ptr child) { if (hasAncestor(child->getId())) { // in that case, we are trying to create a cycle, abort return false; } if (auto oldParent = child->parentItem().lock()) { if (oldParent->getId() == m_id) { // no change needed return true; } else { // in that case a call to removeChild should have been carried out qDebug() << "ERROR: trying to append a child that alrealdy has a parent"; return false; } } if (auto ptr = m_model.lock()) { ptr->notifyRowAboutToAppend(shared_from_this()); child->m_depth = m_depth + 1; child->m_parentItem = shared_from_this(); int id = child->getId(); auto it = m_childItems.insert(m_childItems.end(), child); m_iteratorTable[id] = it; registerSelf(child); ptr->notifyRowAppended(child); return true; } qDebug() << "ERROR: Something went wrong when appending child in TreeItem. Model is not available anymore"; Q_ASSERT(false); return false; } void TreeItem::moveChild(int ix, std::shared_ptr child) { if (auto ptr = m_model.lock()) { auto childPtr = child->m_parentItem.lock(); if (childPtr && childPtr->getId() != m_id) { childPtr->removeChild(child); } else { // deletion of child auto it = m_iteratorTable[child->getId()]; m_childItems.erase(it); } ptr->notifyRowAboutToAppend(shared_from_this()); child->m_depth = m_depth + 1; child->m_parentItem = shared_from_this(); int id = child->getId(); auto pos = m_childItems.begin(); std::advance(pos, ix); auto it = m_childItems.insert(pos, child); m_iteratorTable[id] = it; ptr->notifyRowAppended(child); m_isInModel = true; } else { qDebug() << "ERROR: Something went wrong when moving child in TreeItem. Model is not available anymore"; Q_ASSERT(false); } } void TreeItem::removeChild(const std::shared_ptr &child) { if (auto ptr = m_model.lock()) { ptr->notifyRowAboutToDelete(shared_from_this(), child->row()); // get iterator corresponding to child Q_ASSERT(m_iteratorTable.count(child->getId()) > 0); auto it = m_iteratorTable[child->getId()]; // deletion of child m_childItems.erase(it); // clean iterator table m_iteratorTable.erase(child->getId()); child->m_depth = 0; child->m_parentItem.reset(); child->deregisterSelf(); ptr->notifyRowDeleted(); } else { qDebug() << "ERROR: Something went wrong when removing child in TreeItem. Model is not available anymore"; Q_ASSERT(false); } } bool TreeItem::changeParent(std::shared_ptr newParent) { Q_ASSERT(!m_isRoot); if (m_isRoot) return false; std::shared_ptr oldParent; - if (oldParent = m_parentItem.lock()) { + if ((oldParent = m_parentItem.lock())) { oldParent->removeChild(shared_from_this()); } bool res = true; if (newParent) { res = newParent->appendChild(shared_from_this()); if (res) { m_parentItem = newParent; } else if (oldParent) { // something went wrong, we have to reset the parent. bool reverse = oldParent->appendChild(shared_from_this()); Q_ASSERT(reverse); } } return res; } std::shared_ptr TreeItem::child(int row) const { Q_ASSERT(row >= 0 && row < (int)m_childItems.size()); auto it = m_childItems.cbegin(); std::advance(it, row); return (*it); } int TreeItem::childCount() const { return (int)m_childItems.size(); } int TreeItem::columnCount() const { return m_itemData.count(); } QVariant TreeItem::dataColumn(int column) const { return m_itemData.value(column); } std::weak_ptr TreeItem::parentItem() const { return m_parentItem; } int TreeItem::row() const { if (auto ptr = m_parentItem.lock()) { // we compute the distance in the parent's children list auto it = ptr->m_childItems.begin(); return (int)std::distance(it, (decltype(it))ptr->m_iteratorTable.at(m_id)); } return -1; } int TreeItem::depth() const { return m_depth; } int TreeItem::getId() const { return m_id; } bool TreeItem::isInModel() const { return m_isInModel; } void TreeItem::registerSelf(std::shared_ptr self) { for (const auto &child : self->m_childItems) { registerSelf(child); } if (auto ptr = self->m_model.lock()) { ptr->registerItem(self); self->m_isInModel = true; } else { qDebug() << "Error : construction of treeItem failed because parent model is not available anymore"; Q_ASSERT(false); } } void TreeItem::deregisterSelf() { for (const auto &child : m_childItems) { child->deregisterSelf(); } if (m_isInModel) { if (auto ptr = m_model.lock()) { ptr->deregisterItem(m_id, this); m_isInModel = false; } } } bool TreeItem::hasAncestor(int id) { if (m_id == id) { return true; } if (auto ptr = m_parentItem.lock()) { return ptr->hasAncestor(id); } return false; } bool TreeItem::isRoot() const { return m_isRoot; } diff --git a/src/bin/abstractprojectitem.cpp b/src/bin/abstractprojectitem.cpp index 493f5338f..777d4adb8 100644 --- a/src/bin/abstractprojectitem.cpp +++ b/src/bin/abstractprojectitem.cpp @@ -1,262 +1,285 @@ /* 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 #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_jobType(AbstractClipJob::NOJOBTYPE) - , m_jobProgress(0) , m_itemType(type) - , m_isCurrent(false) , m_lock(QReadWriteLock::Recursive) -{ -} - -AbstractProjectItem::AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const QDomElement &description, const std::shared_ptr &model) - : TreeItem(QList(), std::static_pointer_cast(model), false) - , m_name() - , m_description() - , m_thumbnail(QIcon()) - , m_date() - , m_binId(id) - , m_usage(0) - , m_clipStatus(StatusReady) - , m_jobType(AbstractClipJob::NOJOBTYPE) - , m_jobProgress(0) - , m_itemType(type) , m_isCurrent(false) - , m_lock(QReadWriteLock::Recursive) { + 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())); } 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())); } void AbstractProjectItem::removeRef() { m_usage--; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this())); } 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 DataDate: data = QVariant(m_date); break; case UsageCount: data = QVariant(m_usage); break; case ItemTypeRole: data = QVariant(m_itemType); break; case JobType: - data = QVariant(m_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(pCore->jobManager()->getJobType(jobIds[0])); + } else { + data = QVariant::fromValue(JobStatus::NoJob); + } + } break; case JobProgress: - data = QVariant(m_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: - data = QVariant(m_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) { 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); 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; } bool AbstractProjectItem::changeParent(std::shared_ptr newParent) { m_lastParentId.clear(); if (newParent) { m_lastParentId = std::static_pointer_cast(newParent)->clipId(); } return TreeItem::changeParent(newParent); } diff --git a/src/bin/abstractprojectitem.h b/src/bin/abstractprojectitem.h index 031cd5b16..e1e7da70e 100644 --- a/src/bin/abstractprojectitem.h +++ b/src/bin/abstractprojectitem.h @@ -1,226 +1,214 @@ /* 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 "project/jobs/abstractclipjob.h" #include "undohelper.hpp" #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); - /** - * @brief Creates a project item upon project load. - * @param description element for this item. - * @param model pointer to the model this item is added to. - * @param parent parent this item should be added to - * - * We try to read the attributes "name" and "description" - */ - AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const QDomElement &description, const std::shared_ptr &model); 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); /** @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, // 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. */ 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::changeParent that tracks the id of the id of the parent */ bool changeParent(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; QDateTime m_date; QString m_binId; uint m_usage; CLIPSTATUS m_clipStatus; - AbstractClipJob::JOBTYPE m_jobType; - int m_jobProgress; - QString m_jobMessage; 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 32e4437a9..95e4d382a 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3463 +1,3179 @@ /* 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 "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/clipmanager.h" #include "project/dialogs/slideshowclip.h" #include "project/invaliddialog.h" -#include "project/jobs/jobmanager.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 "titler/titlewidget.h" #include "ui_qtextclip_ui.h" #include "undohelper.hpp" #include "utils/KoIconUtils.h" #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, KSharedConfig::openConfig(KdenliveSettings::colortheme())); 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(); - if (jobProgress > 0 || jobProgress == JobWaiting) { + JobStatus status = index.data(AbstractProjectItem::JobStatus).value(); + if (status == JobStatus::Pending || status == JobStatus::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 (jobProgress > 0) { + if (status == JobStatus::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 if (jobProgress == JobWaiting) { + } else { // Draw kind of a pause icon progress.setWidth(3); painter->drawRect(progress); progress.moveLeft(progress.right() + 3); painter->drawRect(progress); } - } else if (jobProgress == JobCrashed) { - 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(txtBounding, Qt::AlignCenter, jobText); - } + } + 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(txtBounding, 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_jobManager(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); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_layout->addWidget(m_toolbar); // 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::updateThumbProgress, this, &Bin::doUpdateThumbsProgress); - connect(m_itemModel.get(), &ProjectItemModel::abortAudioThumb, this, &Bin::slotAbortAudioThumb); - connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::refreshAudioThumbs); - connect(m_itemModel.get(), &ProjectItemModel::reloadProducer, this, &Bin::reloadProducer); connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel); - connect(m_itemModel.get(), &ProjectItemModel::requestAudioThumbs, this, &Bin::requestAudioThumbs); - connect(m_itemModel.get(), &ProjectItemModel::discardJobs, [&](const QString &id, AbstractClipJob::JOBTYPE type){ - if (hasPendingJob(id, type)) - discardJobs(id, type); - }); - connect(m_itemModel.get(), &ProjectItemModel::startJob, this, &Bin::startJob); + 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(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), m_proxyModel, SLOT(slotDataChanged(const QModelIndex &, const QModelIndex &))); connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); connect(m_itemModel.get(), SIGNAL(itemDropped(QStringList, QModelIndex)), this, SLOT(slotItemDropped(QStringList, QModelIndex))); connect(m_itemModel.get(), SIGNAL(itemDropped(QList, QModelIndex)), this, SLOT(slotItemDropped(QList, QModelIndex))); connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited); - connect(m_itemModel.get(), &ProjectItemModel::addClipCut, this, &Bin::slotAddClipCut); 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(KoIconUtils::themedIcon(QStringLiteral("view-list-tree")), i18n("View Mode"), this); pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType); QAction *treeViewAction = listType->addAction(KoIconUtils::themedIcon(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(KoIconUtils::themedIcon(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(KoIconUtils::themedIcon(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, SIGNAL(triggered(QAction *)), this, SLOT(slotInitView(QAction *))); // Settings menu QMenu *settingsMenu = new QMenu(i18n("Settings"), this); settingsMenu->addAction(listType); QMenu *sliderMenu = new QMenu(i18n("Zoom"), this); sliderMenu->setIcon(KoIconUtils::themedIcon(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(KoIconUtils::themedIcon(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); + // 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, &KMessageWidget::linkActivated, this, &Bin::slotShowJobLog); connect(m_infoMessage, &BinMessageWidget::messageClosing, this, &Bin::slotResetInfoMessage); // m_infoMessage->setWordWrap(true); m_infoMessage->hide(); connect(this, SIGNAL(requesteInvalidRemoval(QString, QString, QString)), this, SLOT(slotQueryRemoval(QString, QString, QString))); - connect(this, &Bin::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs); 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::slotAbortAudioThumb(const QString &id, long duration) -{ - if (!m_audioThumbsThread.isRunning()) { - return; - } - QMutexLocker aMutex(&m_audioThumbMutex); - if (m_audioThumbsList.removeAll(id) > 0) { - m_audioDuration -= duration; - } -} - -void Bin::requestAudioThumbs(const QString &id, long duration) -{ - if (!m_audioThumbsList.contains(id) && m_processingAudioThumb != id) { - m_audioThumbMutex.lock(); - m_audioThumbsList.append(id); - m_audioDuration += duration; - m_audioThumbMutex.unlock(); - processAudioThumbs(); - } -} - -void Bin::doUpdateThumbsProgress(long ms) -{ - int progress = int(((long)m_processedAudio + (long)ms) * 100 / (long)m_audioDuration); - emitMessage(i18n("Creating audio thumbnails"), progress, ProcessingJobMessage); -} - -void Bin::processAudioThumbs() -{ - if (m_audioThumbsThread.isRunning()) { - return; - } - m_audioThumbsThread = QtConcurrent::run(this, &Bin::slotCreateAudioThumbs); -} - void Bin::abortOperations() { blockSignals(true); abortAudioThumbs(); if (m_propertiesPanel) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } } delete m_itemView; m_itemView = nullptr; - delete m_jobManager; - m_jobManager = 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: "<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 = KoIconUtils::themedIcon(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 = KoIconUtils::themedIcon(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 - QStringList folderInfo = getFolderInfo(); - ClipCreationDialog::createClipsCommand(m_doc, folderInfo, this); -} - -void Bin::deleteClip(const QString &id) -{ - if (m_monitor->activeClipId() == id) { - emit openClip(std::shared_ptr()); - } - std::shared_ptr clip = m_itemModel->getClipByBinID(id); - if (!clip) { - qCWarning(KDENLIVE_LOG) << "Cannot bin find clip to delete: " << id; - return; - } - m_jobManager->discardJobs(id); - ClipType type = clip->clipType(); - QString url = clip->url(); - m_fileWatcher.removeFile(id, url); - clip->setClipStatus(AbstractProjectItem::StatusDeleting); - if (!m_processingAudioThumb.isEmpty()) { - clip->abortAudioThumbs(); - } - std::shared_ptr parent = clip->parent(); - Q_ASSERT(parent); - parent->removeChild(clip); - m_doc->deleteClip(id, type, url); + 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; }; for (const auto &item : items) { m_itemModel->requestBinClipDeletion(item, undo, redo); } 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() == Playlist) { + 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)); } } } } QDomDocument doc; QDomElement xml = currentItem->toXml(doc); qCDebug(KDENLIVE_LOG) << "*****************\n" << doc.toString() << "\n******************"; if (!xml.isNull()) { currentItem->setClipStatus(AbstractProjectItem::StatusWaiting); // We need to set a temporary id before all outdated producers are replaced; - m_doc->getFileProperties(xml, currentItem->AbstractProjectItem::clipId(), 150, true); + // TODO refac + // m_doc->getFileProperties(xml, currentItem->AbstractProjectItem::clipId(), 150, true); + pCore->jobManager()->startJob({currentItem->AbstractProjectItem::clipId()}, {}, QString(), xml); } } } } 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) { - QStringList folderInfo = getFolderInfo(ix); 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); } - ClipCreationDialog::createClipFromXml(m_doc, xml, folderInfo, this); + QString id; + m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip")); } } } } void Bin::setMonitor(Monitor *monitor) { m_monitor = monitor; connect(m_monitor, SIGNAL(addClipToProject(QUrl)), this, SLOT(slotAddClipToProject(QUrl))); connect(m_monitor, SIGNAL(requestAudioThumb(QString)), this, SLOT(slotSendAudioThumb(QString))); connect(m_monitor, &Monitor::refreshCurrentClip, this, &Bin::slotOpenCurrent); connect(this, &Bin::openClip, [&](std::shared_ptr clip) { m_monitor->slotOpenClip(clip); }); } int Bin::getFreeFolderId() { return m_folderCounter++; } int Bin::getFreeClipId() { return m_clipCounter++; } int Bin::lastClipId() const { return qMax(0, m_clipCounter - 1); } void Bin::setDocument(KdenliveDoc *project) { // Remove clip from Bin's monitor if (m_doc) { emit openClip(std::shared_ptr()); } m_infoMessage->hide(); blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); // Cleanup previous project m_itemModel->clean(); m_fileWatcher.clear(); delete m_itemView; m_itemView = nullptr; - delete m_jobManager; m_clipCounter = 1; m_folderCounter = 1; m_doc = project; int iconHeight = QFontInfo(font()).pixelSize() * 3.5; m_iconSize = QSize(iconHeight * pCore->getCurrentDar(), iconHeight); - m_jobManager = new JobManager(this); setEnabled(true); blockSignals(false); m_proxyModel->selectionModel()->blockSignals(false); - connect(m_jobManager, SIGNAL(addClip(QString, int)), this, SLOT(slotAddUrl(QString, int))); connect(m_proxyAction, SIGNAL(toggled(bool)), m_doc, SLOT(slotProxyCurrentItem(bool))); - connect(m_jobManager, &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount); - connect(m_discardCurrentClipJobs, &QAction::triggered, m_jobManager, &JobManager::slotDiscardClipJobs); - connect(m_cancelJobs, &QAction::triggered, m_jobManager, &JobManager::slotCancelJobs); - connect(m_discardPendingJobs, &QAction::triggered, m_jobManager, &JobManager::slotCancelPendingJobs); - connect(m_jobManager, &JobManager::updateJobStatus, this, &Bin::slotUpdateJobStatus); - - connect(m_jobManager, SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap)), this, - SLOT(slotGotFilterJobResults(QString, int, int, stringMap, stringMap))); // 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::slotAddUrl(const QString &url, int folderId, const QMap &dataMap) -{ - const QList urls = QList() << QUrl::fromLocalFile(url); - QStringList folderInfo; - if (folderId >= 0) { - QModelIndex ix = getIndexForId(QString::number(folderId), true); - if (ix.isValid()) { - folderInfo = getFolderInfo(m_proxyModel->mapFromSource(ix)); - } else { - folderInfo = getFolderInfo(); - } - } - ClipCreationDialog::createClipsCommand(m_doc, urls, folderInfo, this, dataMap); -} - -void Bin::slotAddUrl(const QString &url, const QMap &dataMap) -{ - const QList urls = QList() << QUrl::fromLocalFile(url); - const QStringList folderInfo = getFolderInfo(); - ClipCreationDialog::createClipsCommand(m_doc, urls, folderInfo, this, dataMap); -} - 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::removeSubClip(const QString &id, QUndoCommand *deleteCommand) -{ - // Check parent item - QString clipId = id; - int in = clipId.section(QLatin1Char(':'), 1, 1).toInt(); - int out = clipId.section(QLatin1Char(':'), 2, 2).toInt(); - clipId = clipId.section(QLatin1Char(':'), 0, 0); - new AddBinClipCutCommand(this, clipId, in, out, false, deleteCommand); -} - - 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 type = clip->clipType(); - m_openAction->setEnabled(type == Image || type == Audio || type == Text || type == TextTemplate); + 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, QPair(), QSize(), false); // Display black bg in clip monitor emit openClip(std::shared_ptr()); } } -QList> Bin::selectedClips() +std::vector Bin::selectedClipsIds(bool excludeFolders) { - // TODO: handle clips inside folders const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); - QList> list; + 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)); - auto currentItem = std::static_pointer_cast(item); - if (currentItem) { - list << currentItem; - } + item->accumulate(ids, itemAdder); } - return list; + 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 type = Unknown; + ClipType 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 == Image || type == Audio || type == TextTemplate || type == Text); + 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() == Text || clip->clipType() == TextTemplate) { + 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 Text: - case TextTemplate: + case ClipType::Text: + case ClipType::TextTemplate: showTitleWidget(clip); break; - case SlideShow: + case ClipType::SlideShow: showSlideshowWidget(clip); break; - case QText: - ClipCreationDialog::createQTextClip(m_doc, getFolderInfo(), this, clip.get()); + 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() == SlideShow) { + if (clip->clipType() == ClipType::SlideShow) { m_propertiesPanel->setEnabled(false); showSlideshowWidget(clip); - } else if (clip->clipType() == QText) { + } else if (clip->clipType() == ClipType::QText) { m_propertiesPanel->setEnabled(false); - ClipCreationDialog::createQTextClip(m_doc, getFolderInfo(), this, clip.get()); + 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, SIGNAL(updateClipProperties(QString, QMap, QMap)), this, SLOT(slotEditClipCommand(QString, QMap, QMap))); connect(panel, SIGNAL(seekToFrame(int)), m_monitor, SLOT(slotSeek(int))); 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; } QDomDocument doc; QDomElement xml = clip->toXml(doc); if (!xml.isNull()) { - m_doc->getFileProperties(xml, id, 150, true); + pCore->jobManager()->startJob({id}, {}, QString(), xml); } } void Bin::slotThumbnailReady(const QString &id, const QImage &img, bool fromFile) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setThumbnail(img); // Save thumbnail for later reuse bool ok = false; if (!fromFile && clip->clipStatus() == ProjectClip::StatusReady) { img.save(m_doc->getCacheDir(CacheThumbs, &ok).absoluteFilePath(clip->hash() + QStringLiteral(".png"))); } } } 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 { + } 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); } selectProxyModel(m_proxyModel->mapFromSource(ix)); m_itemView->scrollTo(m_proxyModel->mapFromSource(ix)); emit openClip(clip); } 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) { + // 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::slotShowJobLog() -{ - QDialog d(this); - QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); - QWidget *mainWidget = new QWidget(this); - auto *l = new QVBoxLayout; - QTextEdit t(&d); - for (int i = 0; i < m_errorLog.count(); ++i) { - if (i > 0) { - t.insertHtml(QStringLiteral("


")); - } - t.insertPlainText(m_errorLog.at(i)); - } - t.setReadOnly(true); - l->addWidget(&t); - mainWidget->setLayout(l); - auto *mainLayout = new QVBoxLayout; - d.setLayout(mainLayout); - mainLayout->addWidget(mainWidget); - mainLayout->addWidget(buttonBox); - d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::accept); - d.exec(); -} - void Bin::gotProxy(const QString &id, const QString &path) { + // TODO refac : delete this std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { QDomDocument doc; clip->setProducerProperty(QStringLiteral("kdenlive:proxy"), path); QDomElement xml = clip->toXml(doc, true); if (!xml.isNull()) { - m_doc->getFileProperties(xml, id, 150, true); + pCore->jobManager()->startJob({id}, {}, QString(), xml); } } } -void Bin::reloadProducer(const QString &id, const QDomElement &xml) -{ - m_doc->getFileProperties(xml, id, 150, true); -} - 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::discardJobs(const QString &id, AbstractClipJob::JOBTYPE type) -{ - m_jobManager->discardJobs(id, type); -} - -void Bin::slotStartCutJob(const QString &id) -{ - startJob(id, AbstractClipJob::CUTJOB); -} - -void Bin::startJob(const QString &id, AbstractClipJob::JOBTYPE type) -{ - std::shared_ptr clip = getBinClip(id); - if ((clip != nullptr) && !hasPendingJob(id, type)) { - // Launch job - const QList clips = {clip.get()}; - m_jobManager->prepareJobs(clips, pCore->getCurrentFps(), type); - } -} - -bool Bin::hasPendingJob(const QString &id, AbstractClipJob::JOBTYPE type) -{ - return m_jobManager->hasPendingJob(id, type); -} - 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 type = (ClipType)act->data().toInt(); QStringList folderInfo = getFolderInfo(); QString parentFolder = getCurrentFolder(); switch (type) { - case Color: + case ClipType::Color: ClipCreationDialog::createColorClip(m_doc, parentFolder, m_itemModel); break; - case SlideShow: - ClipCreationDialog::createSlideshowClip(m_doc, folderInfo, this); + case ClipType::SlideShow: + ClipCreationDialog::createSlideshowClip(m_doc, parentFolder, m_itemModel); break; - case Text: + case ClipType::Text: ClipCreationDialog::createTitleClip(m_doc, folderInfo, QString(), this); break; - case TextTemplate: - ClipCreationDialog::createTitleTemplateClip(m_doc, folderInfo, this); + case ClipType::TextTemplate: + ClipCreationDialog::createTitleTemplateClip(m_doc, parentFolder, m_itemModel); break; - case QText: - ClipCreationDialog::createQTextClip(m_doc, folderInfo, this); + 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); 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); 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); } } } 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_ptrclp = std::static_pointer_cast(clip); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, QPair(0, clp->frameDuration()), clp->getFrameSize(), false); return; } if (clip->itemType() == AbstractProjectItem::SubClipItem) { if (auto ptr = clip->parentItem().lock()) { std::shared_ptrclp = std::static_pointer_cast(ptr); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, QPair(0, clp->frameDuration()), clp->getFrameSize(), false); } return; } } emit requestShowEffectStack(QString(), nullptr, QPair(), 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); currentParent->removeChild(currentItem); currentItem->changeParent(newParent); currentItem->updateParentInfo(newParentId, newParent->name()); } 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) { - QStringList folderInfo; + 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(); } - if (parentItem != m_itemModel->getRootFolder()) { - folderInfo << parentItem->clipId(); - } - } - // TODO: verify if urls exist - QList clipsToAdd = urls; - QMimeDatabase db; - for (const QUrl &file : clipsToAdd) { - // Check there is no folder here - QMimeType type = db.mimeTypeForUrl(file); - if (type.inherits(QStringLiteral("inode/directory"))) { - // user dropped a folder, import its files - clipsToAdd.removeAll(file); - QDir dir(file.toLocalFile()); - const QStringList result = dir.entryList(QDir::Files); - QList folderFiles; - folderFiles.reserve(result.count()); - for (const QString &path : result) { - folderFiles.append(QUrl::fromLocalFile(dir.absoluteFilePath(path))); - } - if (!folderFiles.isEmpty()) { - QString folderId = slotAddFolder(dir.dirName()); - QModelIndex ind = getIndexForId(folderId, true); - QStringList newFolderInfo; - if (ind.isValid()) { - newFolderInfo = getFolderInfo(m_proxyModel->mapFromSource(ind)); - } - ClipCreationDialog::createClipsCommand(m_doc, folderFiles, newFolderInfo, this); - } - } - } - if (!clipsToAdd.isEmpty()) { - ClipCreationDialog::createClipsCommand(m_doc, clipsToAdd, folderInfo, this); + parentFolder = parentItem->clipId(); } + ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel); } void Bin::slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command) { - //TODO reimplement this + // 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); } -void Bin::slotStartClipJob(bool enable) -{ - Q_UNUSED(enable) - - QAction *act = qobject_cast(sender()); - if (act == nullptr) { - // Cannot access triggering action, something is wrong - qCDebug(KDENLIVE_LOG) << "// Error in clip job action"; - return; - } - startClipJob(act->data().toStringList()); -} - -void Bin::startClipJob(const QStringList ¶ms) -{ - QStringList paramData = params; - if (paramData.isEmpty()) { - qCDebug(KDENLIVE_LOG) << "// Error in clip job action"; - return; - } - AbstractClipJob::JOBTYPE jobType = (AbstractClipJob::JOBTYPE)paramData.takeFirst().toInt(); - auto clips_ptr = selectedClips(); - QList clips; - for (auto c : clips_ptr) { - clips << c.get(); - } - m_jobManager->prepareJobs(clips, pCore->getCurrentFps(), jobType, paramData); -} - -void Bin::slotCancelRunningJob(const QString &id, const QMap &newProps) -{ - if (newProps.isEmpty()) { - return; - } - std::shared_ptr clip = getBinClip(id); - if (!clip) { - return; - } - QMap oldProps; - QMapIterator i(newProps); - while (i.hasNext()) { - i.next(); - QString value = newProps.value(i.key()); - oldProps.insert(i.key(), value); - } - if (newProps == oldProps) { - return; - } - auto *command = new EditClipCommand(this, id, oldProps, newProps, true); - m_doc->commandStack()->push(command); -} - -void Bin::slotPrepareJobsMenu() -{ - std::shared_ptr item = getFirstSelectedClip(); - if (item) { - const QString &id = item->AbstractProjectItem::clipId(); - m_discardCurrentClipJobs->setData(id); - QStringList jobs = m_jobManager->getPendingJobs(id); - m_discardCurrentClipJobs->setEnabled(!jobs.isEmpty()); - } else { - m_discardCurrentClipJobs->setData(QString()); - m_discardCurrentClipJobs->setEnabled(false); - } -} - -void Bin::slotAddClipCut(const QString &id, int in, int out) -{ - auto *command = new AddBinClipCutCommand(this, id, in, out, true); - m_doc->commandStack()->push(command); -} - -void Bin::addClipCut(const QString &id, int in, int out) -{ - std::shared_ptr clip = getBinClip(id); - if (!clip) { - return; - } - // Check that we don't already have that subclip - std::shared_ptr sub = clip->getSubClip(in, out); - if (sub) { - // A subclip with same zone already exists - return; - } - sub = ProjectSubClip::construct(clip, m_itemModel, in, out, m_doc->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode())); - QList missingThumbs; - missingThumbs << in; - clip->slotExtractImage(missingThumbs); -} - -void Bin::removeClipCut(const QString &id, int in, int out) -{ - std::shared_ptr clip = getBinClip(id); - if (!clip) { - return; - } - std::shared_ptr sub = clip->getSubClip(in, out); - if (sub) { - clip->removeChild(sub); - sub->discard(); - } -} - Timecode Bin::projectTimecode() const { return m_doc->timecode(); } void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &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)); } - m_jobManager->prepareJobFromTimeline(clip.get(), producerParams, filterParams, consumerParams, extraParams); + */ } void Bin::focusBinView() const { m_itemView->setFocus(); } void Bin::slotOpenClip() { std::shared_ptr clip = getFirstSelectedClip(); if (!clip) { return; } switch (clip->clipType()) { - case Text: - case TextTemplate: + case ClipType::Text: + case ClipType::TextTemplate: showTitleWidget(clip); break; - case Image: + 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 Audio: + 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(), + // /* + EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(), true, true); m_commandStack->push(command); - emit clipItemSelected(clip);*/ + emit clipItemSelected(clip); } - // emit gotFilterJobResults(id, startPos, track, results, filterInfo);*/ + // 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 (qAbs(sourceFps) < 1e-4) { 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); + 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) { // TODO refact : look at this -// if (!clipId.isEmpty()) { -// (pCore->projectManager()->currentTimeline()->hideClip(clipId, request)); -// } + // 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 (qAbs(dia_ui.duration() - clip->duration().frames(pCore->getCurrentFps())) > 1e-4) { // 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), + 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::slotCreateAudioThumb(const QString &id) -{ - std::shared_ptr clip = m_itemModel->getClipByBinID(id); - if (!clip) { - return; - } - clip->createAudioThumbs(); -} - 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; }; for (const QString &i : ids) { - deleteClip(i); + auto item = m_itemModel->getClipByBinID(i); + m_itemModel->requestBinClipDeletion(item, undo, redo); } } 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) { // 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 t = clp->clipType(); - if (t == Playlist) { + if (t == ClipType::Playlist) { toProxy << clp; continue; - } else if ((t == AV || t == Video) && m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { + } 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 == Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { + } 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() == AV || clp->clipType() == Video || clp->clipType() == Playlist) { + 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_clipCounter == 1 || 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; - m_doc->getFileProperties(xml, clip->AbstractProjectItem::clipId(), 150, true); + pCore->jobManager()->startJob({clip->AbstractProjectItem::clipId()}, {}, 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); } bool Bin::addClip(QDomElement elem, const QString &clipId) { const QString producerId = clipId.section(QLatin1Char('_'), 0, 0); elem.setAttribute(QStringLiteral("id"), producerId); if ((KdenliveSettings::default_profile().isEmpty() || KdenliveSettings::checkfirstprojectclip()) && isEmpty()) { elem.setAttribute(QStringLiteral("checkProfile"), 1); } createClip(elem); - m_doc->getFileProperties(elem, producerId, 150, true); + pCore->jobManager()->startJob({producerId}, {}, QString(), elem); return true; } 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); } } QVariantList Bin::audioFrameCache(const QString &id) { std::shared_ptr clip = getBinClip(id); if (clip) { return clip->audioFrameCache; } return QVariantList(); } void Bin::setCurrent(std::shared_ptr item) { switch (item->itemType()) { case AbstractProjectItem::ClipItem: { openProducer(std::static_pointer_cast(item)); - std::shared_ptrclp = std::static_pointer_cast(item); + std::shared_ptr clp = std::static_pointer_cast(item); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, QPair(0, clp->frameDuration()), 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(); } -void Bin::prepareTimelineReplacement(const requestClipInfo &info, const std::shared_ptr &producer) -{ - std::shared_ptr clip = m_itemModel->getClipByBinID(info.clipId); - Q_ASSERT(clip != nullptr); - slotProducerReady(info, producer); - clip->replaceInTimeline(); -} - 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; } - 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/bin.h b/src/bin/bin.h index 277c46a46..5a2fb2649 100644 --- a/src/bin/bin.h +++ b/src/bin/bin.h @@ -1,541 +1,495 @@ /* 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 KDENLIVE_BIN_H #define KDENLIVE_BIN_H #include "abstractprojectitem.h" -#include "timecode.h" -#include "filewatcher.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" +#include "filewatcher.hpp" +#include "timecode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class KdenliveDoc; class QVBoxLayout; class QScrollArea; class ClipController; class QDockWidget; class QTimeLine; class QToolBar; class QMenu; class QToolButton; class QUndoCommand; class ProjectItemModel; class ProjectClip; class ProjectFolder; class AbstractProjectItem; class Monitor; class ProjectSortProxyModel; -class JobManager; class ProjectFolderUp; class InvalidDialog; class BinItemDelegate; class BinMessageWidget; class SmallJobLabel; namespace Mlt { class Producer; } class MyListView : public QListView { Q_OBJECT public: explicit MyListView(QWidget *parent = nullptr); protected: void focusInEvent(QFocusEvent *event) override; signals: void focusView(); }; class MyTreeView : public QTreeView { Q_OBJECT Q_PROPERTY(bool editing READ isEditing WRITE setEditing) public: explicit MyTreeView(QWidget *parent = nullptr); void setEditing(bool edit); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void focusInEvent(QFocusEvent *event) override; void keyPressEvent(QKeyEvent *event) override; protected slots: void closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) override; void editorDestroyed(QObject *editor) override; private: QPoint m_startPos; bool m_editing; bool performDrag(); bool isEditing() const; signals: void focusView(); }; class BinMessageWidget : public KMessageWidget { Q_OBJECT public: explicit BinMessageWidget(QWidget *parent = nullptr); BinMessageWidget(const QString &text, QWidget *parent = nullptr); protected: bool event(QEvent *ev) override; signals: void messageClosing(); }; class SmallJobLabel : public QPushButton { Q_OBJECT public: explicit SmallJobLabel(QWidget *parent = nullptr); static const QString getStyleSheet(const QPalette &p); void setAction(QAction *action); private: enum ItemRole { NameRole = Qt::UserRole, DurationRole, UsageRole }; QTimeLine *m_timeLine; QAction *m_action; public slots: void slotSetJobCount(int jobCount); private slots: void slotTimeLineChanged(qreal value); void slotTimeLineFinished(); }; class LineEventEater : public QObject { Q_OBJECT public: explicit LineEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void clearSearchLine(); void showClearButton(bool); }; /** * @class Bin * @brief The bin widget takes care of both item model and view upon project opening. */ class Bin : public QWidget { Q_OBJECT /** @brief Defines the view types (icon view, tree view,...) */ enum BinViewType { BinTreeView, BinIconView }; public: explicit Bin(const std::shared_ptr &model, QWidget *parent = nullptr); ~Bin(); bool isLoading; /** @brief Sets the document for the bin and initialize some stuff */ void setDocument(KdenliveDoc *project); /** @brief Create a clip item from its xml description */ void createClip(const QDomElement &xml); /** @brief Used to notify the Model View that an item was updated */ void emitItemUpdated(std::shared_ptr item); /** @brief Set monitor associated with this bin (clipmonitor) */ void setMonitor(Monitor *monitor); /** @brief Returns the clip monitor */ Monitor *monitor(); /** @brief Open a producer in the clip monitor */ void openProducer(std::shared_ptr controller); void openProducer(std::shared_ptr controller, int in, int out); - /** @brief Trigger deletion of an item */ - void deleteClip(const QString &id); - /** @brief Get a clip from it's id */ std::shared_ptr getBinClip(const QString &id); - /** @brief Returns a list of selected clips */ - QList> selectedClips(); - - /** @brief Start a job of selected type for a clip */ - void startJob(const QString &id, AbstractClipJob::JOBTYPE type); - - /** @brief Discard jobs from a chosen type, use NOJOBTYPE to discard all jobs for this clip */ - void discardJobs(const QString &id, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); - - /** @brief Check if there is a job waiting / running for this clip */ - bool hasPendingJob(const QString &id, AbstractClipJob::JOBTYPE type); + /** @brief Returns a list of selected clip ids + @param excludeFolders: if true, ids of folders are not returned + */ + std::vector selectedClipsIds(bool excludeFolders = true); - /** @brief Reload / replace a producer */ - void reloadProducer(const QString &id, const QDomElement &xml); + // Returns the selected clips + QList> selectedClips(); /** @brief Current producer has changed, refresh monitor and timeline*/ void refreshClip(const QString &id); void setupMenu(QMenu *addMenu, QAction *defaultAction, const QHash &actions); /** @brief The source file was modified, we will reload it soon, disable item in the meantime */ void setWaitingStatus(const QString &id); const QString getDocumentProperty(const QString &key); /** @brief A proxy clip was just created, pass it to the responsible item */ void gotProxy(const QString &id, const QString &path); /** @brief Give a number available for a clip id, used when adding a new clip to the project. Id must be unique */ int getFreeClipId(); /** @brief Give a number available for a folder id, used when adding a new folder to the project. Id must be unique */ int getFreeFolderId(); /** @brief Returns the id of the last inserted clip */ int lastClipId() const; /** @brief Ask MLT to reload this clip's producer */ void reloadClip(const QString &id); - /** @brief Add a folder */ - void removeSubClip(const QString &id, QUndoCommand *deleteCommand); void doMoveClip(const QString &id, const QString &newParentId); void doMoveFolder(const QString &id, const QString &newParentId); void setupGeneratorMenu(); - void startClipJob(const QStringList ¶ms); - - void addClipCut(const QString &id, int in, int out); - void removeClipCut(const QString &id, int in, int out); /** @brief Set focus to the Bin view. */ void focusBinView() const; /** @brief Get a string list of all clip ids that are inside a folder defined by id. */ QStringList getBinFolderClipIds(const QString &id) const; /** @brief Build a rename subclip command. */ void renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out); /** @brief Rename a clip zone (subclip). */ void renameSubClip(const QString &id, const QString &newName, const QString &oldName, int in, int out); /** @brief Returns current project's timecode. */ Timecode projectTimecode() const; /** @brief Trigger timecode format refresh where needed. */ void updateTimecodeFormat(); /** @brief Edit an effect settings to a bin clip. */ void editMasterEffect(std::shared_ptr clip); /** @brief An effect setting was changed, update stack if displayed. */ void updateMasterEffect(ClipController *ctl); /** @brief Display a message about an operation in status bar. */ void emitMessage(const QString &, int, MessageType); void rebuildMenu(); void refreshIcons(); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); void requestAudioThumbs(const QString &id, long duration); /** @brief Proxy status for the project changed, update. */ void refreshProxySettings(); /** @brief A clip is ready, update its info panel if displayed. */ void emitRefreshPanel(const QString &id); - /** @brief Audio thumbs just finished creating, update on monitor display. */ - void emitRefreshAudioThumbs(const QString &id); /** @brief Returns true if there is no clip. */ bool isEmpty() const; /** @brief Trigger reload of all clips. */ void reloadAllProducers(); /** @brief Get usage stats for project bin. */ void getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize); /** @brief Returns the clip properties dockwidget. */ QDockWidget *clipPropertiesDock(); /** @brief Returns a document's cache dir. ok is set to false if folder does not exist */ QDir getCacheDir(CacheType type, bool *ok) const; /** @brief Command adding a bin clip */ bool addClip(QDomElement elem, const QString &clipId); void rebuildProxies(); /** @brief Return a list of all clips hashes used in this project */ QStringList getProxyHashList(); /** @brief Get info (id, name) of a folder (or the currently selected one) */ const QStringList getFolderInfo(const QModelIndex &selectedIx = QModelIndex()); /** @brief Get binId of the current selected folder */ QString getCurrentFolder(); /** @brief Save a clip zone as MLT playlist */ void saveZone(const QStringList &info, const QDir &dir); QVariantList audioFrameCache(const QString &id); // TODO refac: remove this and call directly the function in ProjectItemModel void cleanup(); private slots: void slotAddClip(); void slotReloadClip(); /** @brief Set sorting column */ void slotSetSorting(); /** @brief Show/hide date column */ void slotShowDateColumn(bool show); void slotShowDescColumn(bool show); /** @brief Setup the bin view type (icon view, tree view, ...). - * @param action The action whose data defines the view type or nullptr to keep default view */ + * @param action The action whose data defines the view type or nullptr to keep default view */ void slotInitView(QAction *action); /** @brief Update status for clip jobs */ void slotUpdateJobStatus(const QString &, int, int, const QString &label = QString(), const QString &actionName = QString(), const QString &details = QString()); void slotSetIconSize(int size); void selectProxyModel(const QModelIndex &id); void slotSaveHeaders(); void slotItemDropped(const QStringList &ids, const QModelIndex &parent); void slotItemDropped(const QList &urls, const QModelIndex &parent); void slotEffectDropped(const QStringList &effectData, const QModelIndex &parent); void slotItemEdited(const QModelIndex &, const QModelIndex &, const QVector &); - void slotAddUrl(const QString &url, int folderId, const QMap &data = QMap()); - void slotAddUrl(const QString &url, const QMap &data = QMap()); - void slotPrepareJobsMenu(); - void slotShowJobLog(); - /** @brief process clip job result. */ - void slotGotFilterJobResults(const QString &, int, int, const stringMap &, const stringMap &); /** @brief Reset all text and log data from info message widget. */ void slotResetInfoMessage(); /** @brief Show dialog prompting for removal of invalid clips. */ void slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage); /** @brief Request display of current clip in monitor. */ void slotOpenCurrent(); void slotZoomView(bool zoomIn); /** @brief Widget gained focus, make sure we display effects for master clip. */ void slotGotFocus(); /** @brief Rename a Bin Item. */ void slotRenameItem(); void slotCreateAudioThumbs(); void doRefreshPanel(const QString &id); /** @brief Send audio thumb data to monitor for display. */ void slotSendAudioThumb(const QString &id); void doRefreshAudioThumbs(const QString &id); /** @brief Enable item view and hide message */ void slotMessageActionTriggered(); /** @brief Request editing of title or slideshow clip */ void slotEditClip(); /** @brief Enable / disable clear button on search line * this is a workaround foq Qt bug 54676 */ void showClearButton(bool show); public slots: void slotThumbnailReady(const QString &id, const QImage &img, bool fromFile = false); - /** @brief The producer for this clip is ready. - * @param id the clip id - * @param controller The Controller for this clip - */ - void slotProducerReady(const requestClipInfo &info, std::shared_ptr producer); void slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage); /** @brief Reload clip thumbnail - when frame for thumbnail changed */ void slotRefreshClipThumbnail(const QString &id); void slotDeleteClip(); void slotItemDoubleClicked(const QModelIndex &ix, const QPoint pos); void slotSwitchClipProperties(std::shared_ptr clip); void slotSwitchClipProperties(); /** @brief Creates a new folder with optional name, and returns new folder's id */ QString slotAddFolder(const QString &folderName = QString()); void slotCreateProjectClip(); - /** @brief Start a Cut Clip job on this clip (extract selected zone using FFmpeg) */ - void slotStartCutJob(const QString &id); - /** @brief Triggered by a clip job action, start the job */ - void slotStartClipJob(bool enable); void slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps); - void slotCancelRunningJob(const QString &id, const QMap &newProps); /** @brief Start a filter job requested by a filter applied in timeline */ void slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams); - /** @brief Add a sub clip */ - void slotAddClipCut(const QString &id, int in, int out); /** @brief Open current clip in an external editing application */ void slotOpenClip(); void slotDuplicateClip(); void slotLocateClip(); - /** @brief Request audio thumbnail for clip with id */ - void slotCreateAudioThumb(const QString &id); - /** @brief Abort audio thumbnail for clip with id */ - void slotAbortAudioThumb(const QString &id, long duration); /** @brief Add extra data to a clip. */ void slotAddClipExtraData(const QString &id, const QString &key, const QString &data = QString(), QUndoCommand *groupCommand = nullptr); void slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel); /** @brief Pass some important properties to timeline track producers. */ void updateTimelineProducers(const QString &id, const QMap &passProperties); /** @brief Add effect to active Bin clip (used when double clicking an effect in list). */ void slotAddEffect(QString id, const QStringList &effectData); /** @brief Request current frame from project monitor. * @param clipId is the id of a clip we want to hide from screenshot * @param request true to start capture process, false to end it. It is necessary to emit a false after image is received - **/ + **/ void slotGetCurrentProjectImage(const QString &clipId, bool request); void slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command); void abortAudioThumbs(); /** @brief Abort all ongoing operations to prepare close. */ void abortOperations(); void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions = QList()); /** @brief Reset all clip usage to 0 */ void resetUsageCount(); /** @brief Select a clip in the Bin from its id. */ void selectClipById(const QString &id, int frame = -1, const QPoint &zone = QPoint()); void slotAddClipToProject(const QUrl &url); - void doUpdateThumbsProgress(long ms); void droppedUrls(const QList &urls, const QStringList &folderInfo = QStringList()); - /** @brief A clip producer was changed and needs to be replaced in timeline. */ - void prepareTimelineReplacement(const requestClipInfo &info, const std::shared_ptr &producer); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); + protected: /* This function is called whenever an item is selected to propagate signals (for ex request to show the clip in the monitor) */ void setCurrent(std::shared_ptr item); void selectClip(const std::shared_ptr &clip); void contextMenuEvent(QContextMenuEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override; private: std::shared_ptr m_itemModel; QAbstractItemView *m_itemView; /** @brief An "Up" item that is inserted in bin when using icon view so that user can navigate up */ std::shared_ptr m_folderUp; BinItemDelegate *m_binTreeViewDelegate; ProjectSortProxyModel *m_proxyModel; - JobManager *m_jobManager; QToolBar *m_toolbar; KdenliveDoc *m_doc; FileWatcher m_fileWatcher; QLineEdit *m_searchLine; QToolButton *m_addButton; QMenu *m_extractAudioAction; QMenu *m_transcodeAction; QMenu *m_clipsActionsMenu; QAction *m_inTimelineAction; QAction *m_showDate; QAction *m_showDesc; /** @brief Holds an available unique id for a clip to be created */ int m_clipCounter; /** @brief Holds an available unique id for a folder to be created */ int m_folderCounter; /** @brief Default view type (icon, tree, ...) */ BinViewType m_listType; /** @brief Default icon size for the views. */ QSize m_iconSize; /** @brief Keeps the column width info of the tree view. */ QByteArray m_headerInfo; QVBoxLayout *m_layout; QDockWidget *m_propertiesDock; QScrollArea *m_propertiesPanel; QSlider *m_slider; Monitor *m_monitor; QIcon m_blankThumb; QMenu *m_menu; QAction *m_openAction; QAction *m_editAction; QAction *m_reloadAction; QAction *m_duplicateAction; QAction *m_locateAction; QAction *m_proxyAction; QAction *m_deleteAction; QAction *m_renameAction; QMenu *m_jobsMenu; QAction *m_cancelJobs; QAction *m_discardCurrentClipJobs; QAction *m_discardPendingJobs; SmallJobLabel *m_infoLabel; /** @brief The info widget for failed jobs. */ BinMessageWidget *m_infoMessage; QStringList m_errorLog; InvalidDialog *m_invalidClipDialog; /** @brief Set to true if widget just gained focus (means we have to update effect stack . */ bool m_gainedFocus; /** @brief List of Clip Ids that want an audio thumb. */ QStringList m_audioThumbsList; QString m_processingAudioThumb; QMutex m_audioThumbMutex; /** @brief Total number of milliseconds to process for audio thumbnails */ long m_audioDuration; /** @brief Total number of milliseconds already processed for audio thumbnails */ long m_processedAudio; /** @brief Indicates whether audio thumbnail creation is running. */ QFuture m_audioThumbsThread; void showClipProperties(std::shared_ptr clip, bool forceRefresh = false); /** @brief Get the QModelIndex value for an item in the Bin. */ QModelIndex getIndexForId(const QString &id, bool folderWanted) const; std::shared_ptr getFirstSelectedClip(); void showTitleWidget(std::shared_ptr clip); void showSlideshowWidget(std::shared_ptr clip); void processAudioThumbs(); signals: void itemUpdated(std::shared_ptr); void producerReady(const QString &id); /** @brief Save folder info into MLT. */ void storeFolder(const QString &folderId, const QString &parentId, const QString &oldParentId, const QString &folderName); void gotFilterJobResults(const QString &, int, int, stringMap, stringMap); /** @brief Trigger timecode format refresh where needed. */ void refreshTimeCode(); /** @brief Request display of effect stack for a Bin clip. */ void requestShowEffectStack(const QString &clipName, std::shared_ptr, QPair range, QSize frameSize, bool showKeyframes); /** @brief Request that the given clip is displayed in the clip monitor */ void requestClipShow(std::shared_ptr); void displayBinMessage(const QString &, KMessageWidget::MessageType); void displayMessage(const QString &, int, MessageType); void requesteInvalidRemoval(const QString &, const QString &, const QString &); /** @brief Analysis data changed, refresh panel. */ void updateAnalysisData(const QString &); void openClip(std::shared_ptr c, int in = -1, int out = -1); /** @brief Fill context menu with occurrences of this clip in timeline. */ - void findInTimeline(const QString &, QList ids = QList ()); + void findInTimeline(const QString &, QList ids = QList()); void clipNameChanged(const QString &); /** @brief A clip was updated, request panel update. */ void refreshPanel(const QString &id); - /** @brief A clip audio data was updated, request refresh. */ - void refreshAudioThumbs(const QString &id); }; #endif diff --git a/src/bin/bincommands.cpp b/src/bin/bincommands.cpp index 8875731b4..a48964a98 100644 --- a/src/bin/bincommands.cpp +++ b/src/bin/bincommands.cpp @@ -1,176 +1,116 @@ /*************************************************************************** * Copyright (C) 2015 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 "bincommands.h" #include "bin.h" #include MoveBinClipCommand::MoveBinClipCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) , m_clipId(clipId) , m_oldParentId(oldParentId) , m_newParentId(newParentId) { setText(i18n("Move Clip")); } // virtual void MoveBinClipCommand::undo() { m_bin->doMoveClip(m_clipId, m_oldParentId); } // virtual void MoveBinClipCommand::redo() { m_bin->doMoveClip(m_clipId, m_newParentId); } MoveBinFolderCommand::MoveBinFolderCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) , m_clipId(clipId) , m_oldParentId(oldParentId) , m_newParentId(newParentId) { setText(i18n("Move Clip")); } // virtual void MoveBinFolderCommand::undo() { m_bin->doMoveFolder(m_clipId, m_oldParentId); } // virtual void MoveBinFolderCommand::redo() { m_bin->doMoveFolder(m_clipId, m_newParentId); } RenameBinSubClipCommand::RenameBinSubClipCommand(Bin *bin, const QString &clipId, const QString &newName, const QString &oldName, int in, int out, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) , m_clipId(clipId) , m_oldName(oldName) , m_newName(newName) , m_in(in) , m_out(out) { setText(i18n("Rename Zone")); } // virtual void RenameBinSubClipCommand::undo() { m_bin->renameSubClip(m_clipId, m_oldName, m_newName, m_in, m_out); } // virtual void RenameBinSubClipCommand::redo() { m_bin->renameSubClip(m_clipId, m_newName, m_oldName, m_in, m_out); } -AddBinClipCutCommand::AddBinClipCutCommand(Bin *bin, const QString &clipId, int in, int out, bool add, QUndoCommand *parent) - : QUndoCommand(parent) - , m_bin(bin) - , m_clipId(clipId) - , m_in(in) - , m_out(out) - , m_addCut(add) -{ - setText(i18n("Add Sub Clip")); -} - -// virtual -void AddBinClipCutCommand::undo() -{ - if (m_addCut) { - m_bin->removeClipCut(m_clipId, m_in, m_out); - } else { - m_bin->addClipCut(m_clipId, m_in, m_out); - } -} -// virtual -void AddBinClipCutCommand::redo() -{ - if (m_addCut) { - m_bin->addClipCut(m_clipId, m_in, m_out); - } else { - m_bin->removeClipCut(m_clipId, m_in, m_out); - } -} EditClipCommand::EditClipCommand(Bin *bin, const QString &id, const QMap &oldparams, const QMap &newparams, bool doIt, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) , m_oldparams(oldparams) , m_newparams(newparams) , m_id(id) , m_doIt(doIt) , m_firstExec(true) { setText(i18n("Edit clip")); } // virtual void EditClipCommand::undo() { m_bin->slotUpdateClipProperties(m_id, m_oldparams, true); } // virtual void EditClipCommand::redo() { if (m_doIt) { m_bin->slotUpdateClipProperties(m_id, m_newparams, !m_firstExec); } m_doIt = true; m_firstExec = false; } -AddClipCommand::AddClipCommand(Bin *bin, const QDomElement &xml, const QString &id, bool doIt, QUndoCommand *parent) - : QUndoCommand(parent) - , m_bin(bin) - , m_xml(xml) - , m_id(id) - , m_doIt(doIt) -{ - if (doIt) { - setText(i18n("Add clip")); - } else { - setText(i18n("Delete clip")); - } -} -// virtual -void AddClipCommand::undo() -{ - if (m_doIt) { - m_bin->deleteClip(m_id); - } else { - m_bin->addClip(m_xml, m_id); - } -} -// virtual -void AddClipCommand::redo() -{ - if (m_doIt) { - m_bin->addClip(m_xml, m_id); - } else { - m_bin->deleteClip(m_id); - } -} diff --git a/src/bin/bincommands.h b/src/bin/bincommands.h index ee7da0002..319d8002f 100644 --- a/src/bin/bincommands.h +++ b/src/bin/bincommands.h @@ -1,126 +1,99 @@ /*************************************************************************** * Copyright (C) 2015 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 BINCOMMANDS_H #define BINCOMMANDS_H #include #include #include class Bin; class MoveBinClipCommand : public QUndoCommand { public: explicit MoveBinClipCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldParentId; QString m_newParentId; }; class MoveBinFolderCommand : public QUndoCommand { public: explicit MoveBinFolderCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldParentId; QString m_newParentId; }; class RenameBinSubClipCommand : public QUndoCommand { public: explicit RenameBinSubClipCommand(Bin *bin, const QString &clipId, const QString &newName, const QString &oldName, int in, int out, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldName; QString m_newName; int m_in; int m_out; }; -class AddBinClipCutCommand : public QUndoCommand -{ -public: - explicit AddBinClipCutCommand(Bin *bin, const QString &clipId, int in, int out, bool add, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - Bin *m_bin; - QString m_clipId; - int m_in; - int m_out; - bool m_addCut; -}; class EditClipCommand : public QUndoCommand { public: EditClipCommand(Bin *bin, const QString &id, const QMap &oldparams, const QMap &newparams, bool doIt, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QMap m_oldparams; QMap m_newparams; QString m_id; /** @brief Should this command be executed on first redo ? TODO: we should refactor the code to get rid of this and always execute actions through the *command system. *. */ bool m_doIt; /** @brief This value is true is this is the first time we execute the command, false otherwise. This allows us to refresh the properties panel * only on the later executions of the command, since on the first execution, the properties panel already contains the correct info. */ bool m_firstExec; }; -class AddClipCommand : public QUndoCommand -{ -public: - AddClipCommand(Bin *bin, const QDomElement &xml, const QString &id, bool doIt, QUndoCommand *parent = nullptr); - void undo() override; - void redo() override; - -private: - Bin *m_bin; - QDomElement m_xml; - QString m_id; - bool m_doIt; -}; #endif diff --git a/src/bin/clipcreator.cpp b/src/bin/clipcreator.cpp index dbdb09b28..5a1324001 100644 --- a/src/bin/clipcreator.cpp +++ b/src/bin/clipcreator.cpp @@ -1,46 +1,255 @@ /*************************************************************************** * 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 "clipcreator.hpp" +#include "core.h" +#include "doc/kdenlivedoc.h" +#include "kdenlivesettings.h" +#include "klocalizedstring.h" +#include "macros.hpp" #include "projectitemmodel.h" +#include "titler/titledocument.h" +#include "utils/devices.hpp" #include "xml/xml.hpp" -#include "klocalizedstring.h" +#include +#include #include +#include +#include -QString ClipCreator::createColorClip(const QString& color, int duration, const QString& name, const QString& parentFolder, std::shared_ptr model) +namespace { +QDomElement createProducer(QDomDocument &xml, ClipType type, const QString &resource, const QString &name, int duration, const QString &service, + const QString &transparency) { - QDomDocument xml; QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); - prod.setAttribute(QStringLiteral("type"), (int) Color); + prod.setAttribute(QStringLiteral("type"), (int)type); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("length"), duration); - QMap properties; - properties.insert(QStringLiteral("resource"), color); - properties.insert(QStringLiteral("kdenlive:clipname"), name); - properties.insert(QStringLiteral("mlt_service"), QStringLiteral("color")); + if (!transparency.isEmpty()) { + prod.setAttribute(QStringLiteral("transparency"), transparency); + } + std::unordered_map properties; + properties[QStringLiteral("resource")] = resource; + if (!name.isEmpty()) { + properties[QStringLiteral("kdenlive:clipname")] = name; + } + if (!service.isEmpty()) { + properties[QStringLiteral("mlt_service")] = service; + } Xml::addXmlProperties(prod, properties); + return prod; +} + +} // namespace + +QString ClipCreator::createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, + std::shared_ptr model) +{ + QDomDocument xml; + + auto prod = createProducer(xml, ClipType::Color, color, name, duration, QStringLiteral("color"), QString()); QString id; - model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create color clip")); - return id; + bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create color clip")); + return res ? id : QStringLiteral("-1"); } +QString ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model, Fun &undo, Fun &redo) +{ + QDomDocument xml; + QUrl url(path); + QMimeDatabase db; + QMimeType type = db.mimeTypeForUrl(url); + + qDebug() << "/////////// createClipFromFile"<currentDoc()->getFramePos(KdenliveSettings::image_duration()); + prod = createProducer(xml, ClipType::Image, path, QString(), duration, QString(), QString()); + } else if (type.inherits(QStringLiteral("application/x-kdenlivetitle"))) { + // opening a title file + QDomDocument txtdoc(QStringLiteral("titledocument")); + QFile txtfile(url.toLocalFile()); + if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { + txtfile.close(); + // extract embedded images + QDomNodeList items = txtdoc.elementsByTagName(QStringLiteral("content")); + for (int j = 0; j < items.count(); ++j) { + QDomElement content = items.item(j).toElement(); + if (content.hasAttribute(QStringLiteral("base64"))) { + QString titlesFolder = pCore->currentDoc()->projectDataFolder() + QStringLiteral("/titles/"); + QString imgPath = TitleDocument::extractBase64Image(titlesFolder, content.attribute(QStringLiteral("base64"))); + if (!imgPath.isEmpty()) { + content.setAttribute(QStringLiteral("url"), imgPath); + content.removeAttribute(QStringLiteral("base64")); + } + } + } + prod.setAttribute(QStringLiteral("in"), 0); + int duration = 0; + if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { + duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); + } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { + duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); + } + if (duration <= 0) { + duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()) - 1; + } + prod = createProducer(xml, ClipType::Text, path, QString(), duration, QString(), QString()); + txtdoc.documentElement().setAttribute(QStringLiteral("duration"), duration); + QString titleData = txtdoc.toString(); + prod.setAttribute(QStringLiteral("xmldata"), titleData); + } else { + txtfile.close(); + return QStringLiteral("-1"); + } + } else { + // it is a "normal" file, just use a producer + prod = xml.createElement(QStringLiteral("producer")); + xml.appendChild(prod); + QMap properties; + properties.insert(QStringLiteral("resource"), path); + Xml::addXmlProperties(prod, properties); + qDebug() << "/////////// normal"<requestAddBinClip(id, xml.documentElement(), parentFolder, undo, redo); + return res ? id : QStringLiteral("-1"); +} + +bool ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model) +{ + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + auto id = ClipCreator::createClipFromFile(path, parentFolder, model, undo, redo); + bool ok = (id != QStringLiteral("-1")); + if (ok) { + pCore->pushUndo(undo, redo, i18n("Add clip")); + } + return ok; +} + +QString ClipCreator::createSlideshowClip(const QString &path, int duration, const QString &name, const QString &parentFolder, + const std::unordered_map &properties, std::shared_ptr model) +{ + QDomDocument xml; + + auto prod = createProducer(xml, ClipType::SlideShow, path, name, duration, QString(), QString()); + Xml::addXmlProperties(prod, properties); + + QString id; + bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create slideshow clip")); + return res ? id : QStringLiteral("-1"); +} + +QString ClipCreator::createTitleTemplate(const QString &path, const QString &text, const QString &name, const QString &parentFolder, + std::shared_ptr model) +{ + QDomDocument xml; + + // We try to retrieve duration for template + int duration = 0; + QDomDocument titledoc; + QFile txtfile(path); + if (txtfile.open(QIODevice::ReadOnly) && titledoc.setContent(&txtfile)) { + if (titledoc.documentElement().hasAttribute(QStringLiteral("duration"))) { + duration = titledoc.documentElement().attribute(QStringLiteral("duration")).toInt(); + } else { + // keep some time for backwards compatibility - 26/12/12 + duration = titledoc.documentElement().attribute(QStringLiteral("out")).toInt(); + } + } + txtfile.close(); + + // Duration not found, we fall-back to defaults + if (duration == 0) { + duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()); + } + auto prod = createProducer(xml, ClipType::TextTemplate, path, name, duration, QString(), QStringLiteral("1")); + if (!text.isEmpty()) { + prod.setAttribute(QStringLiteral("templatetext"), text); + } + + QString id; + bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create title template")); + return res ? id : QStringLiteral("-1"); +} + +bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model, + Fun &undo, Fun &redo) +{ + qDebug() << "/////////// creatclipsfromlist"< folderFiles; + for (const QString &path : result) { + folderFiles.append(QUrl::fromLocalFile(dir.absoluteFilePath(path))); + } + QString folderId; + Fun local_undo = []() { return true; }; + Fun local_redo = []() { return true; }; + bool ok = pCore->projectItemModel()->requestAddFolder(folderId, dir.dirName(), parentFolder, local_undo, local_redo); + if (ok) { + ok = createClipsFromList(folderFiles, checkRemovable, folderId, model, local_undo, local_redo); + if (!ok) { + local_undo(); + } else { + UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); + } + } + continue; + } + if (checkRemovable && isOnRemovableDevice(file) && !isOnRemovableDevice(pCore->currentDoc()->projectDataFolder())) { + int answer = KMessageBox::messageBox( + QApplication::activeWindow(), KMessageBox::DialogType::WarningContinueCancel, + i18n("Clip %1
is on a removable device, will not be available when device is unplugged or mounted at a different position. You " + "may want to copy it first to your hard-drive. Would you like to add it anyways?", + file.path()), + i18n("Removable device"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), QStringLiteral("removable")); + + if (answer == KMessageBox::Cancel) continue; + } + QString id = ClipCreator::createClipFromFile(file.toLocalFile(), parentFolder, model, undo, redo); + created = created || (id != QStringLiteral("-1")); + } + qDebug() << "/////////// creatclipsfromlist return"; + return created; +} + +bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model) +{ + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + bool ok = ClipCreator::createClipsFromList(list, checkRemovable, parentFolder, model, undo, redo); + if (ok) { + pCore->pushUndo(undo, redo, i18np("Add clip", "Add clips", list.size())); + } + return ok; +} diff --git a/src/bin/clipcreator.hpp b/src/bin/clipcreator.hpp index 0693a797a..22ac81d5c 100644 --- a/src/bin/clipcreator.hpp +++ b/src/bin/clipcreator.hpp @@ -1,45 +1,88 @@ /*************************************************************************** * 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 CLIPCREATOR_H #define CLIPCREATOR_H +#include "definitions.h" +#include "undohelper.hpp" #include #include +#include -/** @brief This namespace provides convenienc function to create clips based on various parameters +/** @brief This namespace provides convenience functions to create clips based on various parameters */ class ProjectItemModel; -namespace ClipCreator -{ - /* @brief Create and inserts a color clip - @param color : a string of the form "0xff0000ff" (solid red in RGBA) - @param duration : duration expressed in number of frames - @param name: name of the clip - @param parentFolder: the binId of the containing folder - @param model: a shared pointer to the bin item model - @return the binId of the created clip - */ - QString createColorClip(const QString& color, int duration, const QString& name, const QString& parentFolder, std::shared_ptr model); -} +namespace ClipCreator { +/* @brief Create and inserts a color clip + @param color : a string of the form "0xff0000ff" (solid red in RGBA) + @param duration : duration expressed in number of frames + @param name: name of the clip + @param parentFolder: the binId of the containing folder + @param model: a shared pointer to the bin item model + @return the binId of the created clip +*/ +QString createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, std::shared_ptr model); + +/* @brief Create a title template + @param path : path to the template + @param text : text of the template (optional) + @param name: name of the clip + @param parentFolder: the binId of the containing folder + @param model: a shared pointer to the bin item model + @return the binId of the created clip +*/ +QString createTitleTemplate(const QString &path, const QString &text, const QString &name, const QString &parentFolder, + std::shared_ptr model); + +/* @brief Create a slideshow clip + @param path : path to the selected folder + @param duration: this should be nbr of images * duration of one image + @param name: name of the clip + @param parentFolder: the binId of the containing folder + @param properties: description of the slideshow + @param model: a shared pointer to the bin item model + @return the binId of the created clip +*/ +QString createSlideshowClip(const QString &path, int duration, const QString &name, const QString &parentFolder, + const std::unordered_map &properties, std::shared_ptr model); +/* @brief Reads a file from disk and create the corresponding clip + @param path : path to the file + @param parentFolder: the binId of the containing folder + @param model: a shared pointer to the bin item model + @return the binId of the created clip +*/ +QString createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model, Fun &undo, Fun &redo); +bool createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model); + +/* @brief Iterates recursively through the given url list and add the files it finds, recreating a folder structure + @param list: the list of items (can be folders) + @param checkRemovable: if true, it will check if files are on removable devices, and warn the user if so + @param parentFolder: the binId of the containing folder + @param model: a shared pointer to the bin item model + */ +bool createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model, Fun &undo, + Fun &redo); +bool createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model); +} // namespace ClipCreator #endif diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index a9c1ccea6..897f69e59 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1385 +1,981 @@ /* 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 "bin.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" +#include "jobs/jobmanager.h" +#include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/bincontroller.h" -#include "mltcontroller/clipcontroller.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 "effects/effectstack/model/effectstackmodel.hpp" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/KoIconUtils.h" +#include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" -#include #include +#include #include "kdenlive_debug.h" #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, pCore->binController(), producer) - , m_abortAudioThumb(false) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_date = date; m_description = ClipController::description(); - if (m_clipType == Audio) { + if (m_clipType == ClipType::Audio) { m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); - connect(this, &ProjectClip::updateJobStatus, this, &ProjectClip::setJobStatus); - connect(this, &ProjectClip::updateThumbProgress, model.get(), &ProjectItemModel::updateThumbProgress); - connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&](){ - setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); - }); - connectEffectStack(); + 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)); + QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), + Q_ARG(bool, false)); } } // 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); 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, description, model) + : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, pCore->binController()) - , m_abortAudioThumb(false) , 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)description.attribute(QStringLiteral("type")).toInt(); - if (m_clipType == Audio) { + if (m_clipType == ClipType::Audio) { m_thumbnail = KoIconUtils::themedIcon(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(this, &ProjectClip::updateJobStatus, this, &ProjectClip::setJobStatus); - connect(this, &ProjectClip::updateThumbProgress, model.get(), &ProjectItemModel::updateThumbProgress); - connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&](){ - setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); - }); + connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); connectEffectStack(); } -std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) +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 - abortAudioThumbs(); - QMutexLocker audioLock(&m_audioMutex); m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); delete m_thumbsProducer; audioFrameCache.clear(); // delete all timeline producers std::map>::iterator itr = m_timelineProducers.begin(); while (itr != m_timelineProducers.end()) { itr = m_timelineProducers.erase(itr); } } 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::modelChanged, [&](){ qDebug()<<"/ / / STACK CHANGED"; updateChildProducers(); });*/ } -void ProjectClip::abortAudioThumbs() -{ - m_abortAudioThumb = true; - emit doAbortAudioThumbs(); -} 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(const QVariantList &audioLevels) +void ProjectClip::updateAudioThumbnail(QVariantList audioLevels) { - audioFrameCache = 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); emit gotAudioData(); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType 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 != Color) && (m_clipType != Unknown)) { + 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) { - QDomDocument doc; - QDomElement xml = toXml(doc); + // we find if there are some loading job on that clip + int loadjobId = -1; + pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); if (refreshOnly) { - // set a special flag to request thumbnail only - xml.setAttribute(QStringLiteral("refreshOnly"), QStringLiteral("1")); + // 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 + std::vector parentJobs; + if (loadjobId != -1) parentJobs.push_back(loadjobId); + pCore->jobManager()->startJob({clipId()}, parentJobs, QString(), 150, -1, true); + + } else { + // TODO refac: launch load job + // if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->reloadProducer(m_binId, xml); } - if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->reloadProducer(m_binId, xml); } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta) { getProducerXML(document, includeMeta); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); - if (m_clipType != Unknown) { + 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); updateTimelineClips(QVector() << TimelineModel::ReloadThumb); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this())); } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { + qDebug() << "################### ProjectClip::setproducer"; updateProducer(std::move(producer)); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); - if (m_clipType == Audio) { + if (m_clipType == ClipType::Audio) { m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic")); - } else if (m_clipType == Image) { + } 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.")); + 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())); } // Make sure we have a hash for this clip getFileHash(); - createAudioThumbs(); - if (replaceProducer) { - // Recreate thumbnail - } return true; } -void ProjectClip::createAudioThumbs() -{ - if (KdenliveSettings::audiothumbnails() && (m_clipType == AV || m_clipType == Audio || m_clipType == Playlist)) { - if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->requestAudioThumbs(m_binId, duration().ms()); - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobWaiting, 0); - } -} - Mlt::Producer *ProjectClip::thumbProducer() { QMutexLocker locker(&m_producerMutex); if (m_thumbsProducer) { return m_thumbsProducer; } - if (clipType() == Unknown) { + if (clipType() == ClipType::Unknown) { return nullptr; } std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } Clip clip(*prod.get()); if (KdenliveSettings::gpu_accel()) { m_thumbsProducer = 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 = clip.clone(); } return m_thumbsProducer; } std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { return std::shared_ptr(originalProducer()->cut()); } 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::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(*m_masterProducer->profile(), "xml-string", clipXml.constData())); 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() - 1; } 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 SlideShow: + case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; - case Text: + case ClipType::Text: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; - case QText: + case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; - case Color: + 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()) { 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) { QMapIterator 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())); 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"); while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); - if (m_clipType == SlideShow && keys.contains(i.key())) { + 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 if (auto ptr = m_model.lock()) { - emit std::static_pointer_cast(ptr)->discardJobs(m_binId, AbstractClipJob::PROXYJOB); + // TODO refac + // emit std::static_pointer_cast(ptr)->discardJobs(m_binId, AbstractClipJob::PROXYJOB); reloadProducer(); } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); if (auto ptr = m_model.lock()) { - emit std::static_pointer_cast(ptr)->startJob(m_binId, AbstractClipJob::PROXYJOB); + // TODO refac + // emit std::static_pointer_cast(ptr)->startJob(m_binId, AbstractClipJob::PROXYJOB); } } } else if (properties.contains(QStringLiteral("resource")) || properties.contains(QStringLiteral("templatetext")) || properties.contains(QStringLiteral("autorotate"))) { // Clip resource changed, update thumbnail - if (m_clipType != Color) { + if (m_clipType != ClipType::Color) { reloadProducer(); } else { reload = true; } } 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())); 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())); } // 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 (!passProperties.isEmpty()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->updateTimelineProducers(m_binId, passProperties); } } -void ProjectClip::setJobStatus(int jobType, int status, int progress, const QString &statusMessage) -{ - m_jobType = (AbstractClipJob::JOBTYPE)jobType; - if (progress > 0) { - if (m_jobProgress == progress) { - return; - } - m_jobProgress = progress; - } else { - m_jobProgress = status; - if (m_jobType == AbstractClipJob::PROXYJOB && (status == JobAborted || status == JobCrashed)) { - setProducerProperty(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); - } - if ((status == JobAborted || status == JobCrashed || status == JobDone) && !statusMessage.isEmpty()) { - m_jobMessage = statusMessage; - if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->emitMessage(statusMessage, 100, OperationCompletedMessage); - } - } - if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this())); -} - ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); - ClipPropertiesController *panel = - new ClipPropertiesController(static_cast(this), parent); + 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::updateParentInfo(const QString &folderid, const QString &foldername) { Q_UNUSED(foldername); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), folderid); } 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 == TextTemplate) { + 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 hasEffects() ? QVariant("kdenlive-track_has_effect") : QVariant(); break; default: break; } return AbstractProjectItem::getData(type); }*/ -void ProjectClip::slotQueryIntraThumbs(const QList &frames) -{ - QMutexLocker lock(&m_intraThumbMutex); - for (int i = 0; i < frames.count(); i++) { - if (!m_intraThumbs.contains(frames.at(i))) { - m_intraThumbs << frames.at(i); - } - } - qSort(m_intraThumbs); - if (!m_intraThread.isRunning()) { - m_intraThread = QtConcurrent::run(this, &ProjectClip::doExtractIntra); - } -} - -void ProjectClip::doExtractIntra() -{ - Mlt::Producer *prod = thumbProducer(); - if (prod == nullptr || !prod->is_valid()) { - return; - } - int fullWidth = 150 * prod->profile()->dar() + 0.5; - int max = prod->get_length(); - int pos; - while (!m_intraThumbs.isEmpty()) { - m_intraThumbMutex.lock(); - pos = m_intraThumbs.takeFirst(); - m_intraThumbMutex.unlock(); - if (pos >= max) { - pos = max - 1; - } - const QString path = url() + QLatin1Char('_') + QString::number(pos); - auto ptr = m_model.lock(); - Q_ASSERT(ptr); - QImage img; - std::static_pointer_cast(ptr)->m_pixmapCache->findImage(path, &img); - if (!img.isNull()) { - // Cache already contains image - 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, fullWidth, 150); - std::static_pointer_cast(ptr)->m_pixmapCache->insertImage(path, img); - emit thumbReady(pos, img); - } - delete frame; - } -} - 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 Mlt::Producer *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; - std::static_pointer_cast(ptr)->m_pixmapCache->findImage(path, &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, prod->profile()->sar() != 1); - std::static_pointer_cast(ptr)->m_pixmapCache->insertImage(path, img); + 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() { - abortAudioThumbs(); - QString audioThumbPath = getAudioThumbPath(audioInfo()); + QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; - m_abortAudioThumb = false; + pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } -const QString ProjectClip::getAudioThumbPath(AudioStreamInfo *audioInfo) +const QString ProjectClip::getAudioThumbPath() { - if (audioInfo == nullptr) { + if (audioInfo() == nullptr) { return QString(); } - int audioStream = audioInfo->ffmpeg_audio_index(); + int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; - auto ptr = m_model.lock(); - Q_ASSERT(ptr); 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())); + audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } -void ProjectClip::slotCreateAudioThumbs() -{ - QMutexLocker lock(&m_audioMutex); - std::shared_ptr prod = originalProducer(); - if (!prod || !prod->is_valid()) { - return; - } - QString audioPath = getAudioThumbPath(m_audioInfo); - if (audioPath.isEmpty()) { - return; - } - int audioStream = m_audioInfo->ffmpeg_audio_index(); - int lengthInFrames = prod->get_length(); - int frequency = m_audioInfo->samplingRate(); - if (frequency <= 0) { - frequency = 48000; - } - int channels = m_audioInfo->channels(); - if (channels <= 0) { - channels = 2; - } - QVariantList audioLevels; - QImage image(audioPath); - if (!image.isNull()) { - // convert cached image - int n = image.width() * image.height(); - for (int i = 0; i < n; i++) { - QRgb p = image.pixel(i / channels, i % channels); - audioLevels << qRed(p); - audioLevels << qGreen(p); - audioLevels << qBlue(p); - audioLevels << qAlpha(p); - } - } - if (!audioLevels.isEmpty()) { - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0); - updateAudioThumbnail(audioLevels); - return; - } - bool jobFinished = false; - if (KdenliveSettings::ffmpegaudiothumbnails() && m_clipType != Playlist) { - QStringList args; - QList channelFiles; - for (int i = 0; i < channels; i++) { - auto *channelTmpfile = new QTemporaryFile; - if (!channelTmpfile->open()) { - delete channelTmpfile; - if (auto ptr = m_model.lock()) - emit std::static_pointer_cast(ptr)->emitMessage(i18n("Cannot create temporary file, check disk space and permissions"), - 100, ErrorMessage); - return; - } - channelTmpfile->close(); - channelFiles << channelTmpfile; - } - args << QStringLiteral("-i") << QUrl::fromLocalFile(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")); - - if (channels == 1) { - if (isFFmpeg) { - args << QStringLiteral("-filter_complex:a") << QStringLiteral("aformat=channel_layouts=mono,aresample=async=100"); - args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(audioStream > 0 ? ":" + QString::number(audioStream) : QString()) - << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") - << channelFiles[0]->fileName(); - } else { - args << QStringLiteral("-filter_complex:a") << QStringLiteral("aformat=channel_layouts=mono:sample_rates=100"); - args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(audioStream > 0 ? ":" + QString::number(audioStream) : QString()) - << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("s16le") - << channelFiles[0]->fileName(); - } - } else if (channels == 2) { - if (isFFmpeg) { - args << QStringLiteral("-filter_complex:a") - << QStringLiteral("[0:a%1]aresample=async=100,channelsplit=channel_layout=stereo[0:0][0:1]") - .arg(audioStream > 0 ? ":" + QString::number(audioStream) : QString()); - // Channel 1 - args << QStringLiteral("-map") << QStringLiteral("[0:1]") << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") - << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[0]->fileName(); - // Channel 2 - args << QStringLiteral("-map") << QStringLiteral("[0:0]") << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") - << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[1]->fileName(); - } else { - args << QStringLiteral("-filter_complex:a") - << QStringLiteral("[0:a%1]aformat=sample_rates=100,channelsplit=channel_layout=stereo[0:0][0:1]") - .arg(audioStream > 0 ? ":" + QString::number(audioStream) : QString()); - // Channel 1 - args << QStringLiteral("-map") << QStringLiteral("[0:1]") << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") - << QStringLiteral("-f") << QStringLiteral("s16le") << channelFiles[0]->fileName(); - // Channel 2 - args << QStringLiteral("-map") << QStringLiteral("[0:0]") << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") - << QStringLiteral("-f") << QStringLiteral("s16le") << channelFiles[1]->fileName(); - } - } else if (channels == 6) { - args << QStringLiteral("-filter_complex:a") - << QStringLiteral("[0:a%1]aresample=async=100,channelsplit=channel_layout=5.1[0:0][0:1][0:2][0:3][0:4][0:5]") - .arg(audioStream > 0 ? ":" + QString::number(audioStream) : QString()); - for (int i = 0; i < 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[i]->fileName(); - } - } - QProcess audioThumbsProcess; - connect(this, &ProjectClip::doAbortAudioThumbs, &audioThumbsProcess, &QProcess::kill, Qt::DirectConnection); - connect(&audioThumbsProcess, &QProcess::readyReadStandardOutput, this, &ProjectClip::updateFfmpegProgress); - audioThumbsProcess.start(KdenliveSettings::ffmpegpath(), args); - bool ffmpegError = false; - if (!audioThumbsProcess.waitForStarted()) { - ffmpegError = true; - } - audioThumbsProcess.waitForFinished(-1); - if (m_abortAudioThumb) { - // Cleanup temporary ffmpeg audio thumb file - while (!channelFiles.isEmpty()) { - delete channelFiles.takeFirst(); - } - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0); - m_abortAudioThumb = false; - return; - } - - if (!ffmpegError && audioThumbsProcess.exitStatus() != QProcess::CrashExit) { - int dataSize = 0; - QList rawChannels; - QList sourceChannels; - for (int i = 0; i < channelFiles.count(); i++) { - channelFiles[i]->open(); - QByteArray res = channelFiles[i]->readAll(); - channelFiles[i]->close(); - if (dataSize == 0) { - dataSize = res.size(); - } - if (res.isEmpty() || res.size() != dataSize) { - // Something went wrong, abort - // Cleanup temporary ffmpeg audio thumb file - while (!channelFiles.isEmpty()) { - delete channelFiles.takeFirst(); - } - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0); - if (auto ptr = m_model.lock()) - emit std::static_pointer_cast(ptr)->emitMessage(i18n("Error reading audio thumbnail"), 100, ErrorMessage); - return; - } - rawChannels << (const qint16 *)res.constData(); - // We need to keep res2 alive or rawChannels data will die - sourceChannels << res; - } - int progress = 0; - QList channelsData; - double offset = (double)dataSize / (2.0 * 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 < lengthInFrames; i++) { - channelsData.clear(); - for (int k = 0; k < rawChannels.count(); k++) { - channelsData << 0; - } - int pos = (int)(i * offset); - int steps = 0; - for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) { - steps++; - for (int k = 0; k < rawChannels.count(); k++) { - channelsData[k] += abs(rawChannels[k][pos + j]); - } - } - for (int k = 0; k < channelsData.count(); k++) { - if (steps != 0) { - channelsData[k] /= steps; - } - audioLevels << channelsData[k] * factor; - } - int p = 80 + (i * 20 / lengthInFrames); - if (p != progress) { - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobWorking, p); - progress = p; - } - if (m_abortAudioThumb) { - break; - } - } - jobFinished = true; - } else { - if (auto ptr = m_model.lock()) - emit std::static_pointer_cast(ptr)->emitMessage(i18n("Failed to create FFmpeg audio thumbnails, using MLT"), 100, - ErrorMessage); - } - // Cleanup temporary ffmpeg audio thumb file - while (!channelFiles.isEmpty()) { - delete channelFiles.takeFirst(); - } - } - if (!jobFinished && !m_abortAudioThumb) { - // MLT audio thumbs: slower but safer - QString service = 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(*prod->profile(), service.toUtf8().constData(), prod->get("resource"))); - if (!audioProducer->is_valid()) { - return; - } - audioProducer->set("video_index", "-1"); - Mlt::Filter chans(*prod->profile(), "audiochannels"); - Mlt::Filter converter(*prod->profile(), "audioconvert"); - Mlt::Filter levels(*prod->profile(), "audiolevel"); - audioProducer->attach(chans); - audioProducer->attach(converter); - audioProducer->attach(levels); - - int last_val = 0; - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobWaiting, 0); - double framesPerSecond = audioProducer->get_fps(); - mlt_audio_format audioFormat = mlt_audio_s16; - QStringList keys; - keys.reserve(channels); - for (int i = 0; i < channels; i++) { - keys << "meta.media.audio_level." + QString::number(i); - } - - for (int z = 0; z < lengthInFrames && !m_abortAudioThumb; ++z) { - int val = (int)(100.0 * z / lengthInFrames); - if (last_val != val) { - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobWorking, 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(framesPerSecond, frequency, z); - mltFrame->get_audio(audioFormat, frequency, channels, samples); - for (int channel = 0; channel < channels; ++channel) { - double level = 256 * qMin(mltFrame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0); - audioLevels << level; - } - } else if (!audioLevels.isEmpty()) { - for (int channel = 0; channel < channels; channel++) { - audioLevels << audioLevels.last(); - } - } - if (m_abortAudioThumb) { - break; - } - } - } - - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0); - if (!m_abortAudioThumb) { - updateAudioThumbnail(audioLevels); - } - - if (!m_abortAudioThumb && !audioLevels.isEmpty()) { - // Put into an image for caching. - int count = audioLevels.size(); - image = QImage(lrint((count + 3) / 4.0 / channels), 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(audioLevels.at(4 * i).toInt(), audioLevels.at(4 * i + 1).toInt(), audioLevels.at(4 * i + 2).toInt(), - audioLevels.at(4 * i + 3).toInt()); - } else { - int last = audioLevels.last().toInt(); - int r = (4 * i + 0) < count ? audioLevels.at(4 * i + 0).toInt() : last; - int g = (4 * i + 1) < count ? audioLevels.at(4 * i + 1).toInt() : last; - int b = (4 * i + 2) < count ? audioLevels.at(4 * i + 2).toInt() : last; - int a = last; - p = qRgba(r, g, b, a); - } - image.setPixel(i / channels, i % channels, p); - } - image.save(audioPath); - } - m_abortAudioThumb = false; -} - -void ProjectClip::updateFfmpegProgress() -{ - QProcess *callerProcess = qobject_cast(QObject::sender()); - if (!callerProcess) { - return; - } - QString result = callerProcess->readAllStandardOutput(); - const QStringList lines = result.split(QLatin1Char('\n')); - for (const QString &data : lines) { - if (data.startsWith(QStringLiteral("out_time_ms"))) { - long ms = data.section(QLatin1Char('='), 1).toLong(); - // Update clip progressbar - emit updateJobStatus(AbstractClipJob::THUMBJOB, JobWorking, ms / duration().ms() * 0.08); - // Update general statusbar progressbar - emit updateThumbProgress(ms / 1000); - break; - } - } -} - bool ProjectClip::isTransparent() const { - if (m_clipType == Text) { + if (m_clipType == ClipType::Text) { return true; } - return m_clipType == Image && getProducerIntProperty(QStringLiteral("kdenlive:transparency")) == 1; + 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(); } -QImage ProjectClip::findCachedThumb(int pos) -{ - const QString path = url() + QLatin1Char('_') + QString::number(pos); - auto ptr = m_model.lock(); - Q_ASSERT(ptr); - QImage img; - std::static_pointer_cast(ptr)->m_pixmapCache->findImage(path, &img); - return img; -} - bool ProjectClip::isSplittable() const { - return (m_clipType == AV || m_clipType == Playlist); + 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(m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); setRefCount(m_registeredClips.size()); } -QList ProjectClip::timelineInstances() const +QList ProjectClip::timelineInstances() const { - QList ids; - for(std::map>::const_iterator it = m_registeredClips.begin(); it != m_registeredClips.end(); ++it) { + 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) { - auto toDelete = m_registeredClips; //we cannot use m_registeredClips directly, because it will be modified during loop + auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (auto timeline = clip.second.lock()) { timeline->requestClipDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::updateChildProducers() { // 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 223b293db..00083c3ed 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -1,273 +1,248 @@ /* 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). * */ 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); + 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 clipType() const; bool selfSoftDelete(Fun &undo, Fun &redo) 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. */ Mlt::Producer *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 updateParentInfo(const QString &folderid, const QString &foldername); void setWaitingStatus(const QString &id); /** @brief Returns true if the clip matched a condition, for example vcodec=mpeg1video. */ bool matches(const QString &condition); - /** @brief Create audio thumbnail for this clip. */ - void createAudioThumbs(); /** @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 Abort running audio thumb process if any. */ - void abortAudioThumbs(); /** @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(AudioStreamInfo *audioInfo); - /** @brief Returns a cached pixmap for a frame of this clip */ - QImage findCachedThumb(int pos); - void slotQueryIntraThumbs(const QList &frames); + 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; + QList timelineInstances() const; std::shared_ptr timelineProducer(PlaylistState::ClipState state = PlaylistState::Original, int track = 1); std::shared_ptr cloneProducer(); 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 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);}; + 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: - void updateAudioThumbnail(const QVariantList &audioLevels); + /* @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); - void slotCreateAudioThumbs(); - /** @brief Set the Job status on a clip. - * @param jobType The job type - * @param status The job status (see definitions.h) - * @param progress The job progress (in percents) - * @param statusMessage The job info message */ - void setJobStatus(int jobType, int status, int progress = 0, const QString &statusMessage = QString()); private: - bool m_abortAudioThumb; /** @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; Mlt::Producer *m_thumbsProducer; QMutex m_producerMutex; QMutex m_thumbMutex; - QMutex m_intraThumbMutex; - QMutex m_audioMutex; QFuture m_thumbThread; QList m_requestedThumbs; - QFuture m_intraThread; - QList m_intraThumbs; const QString geometryWithOffset(const QString &data, int offset); void doExtractImage(); - void doExtractIntra(); void updateTimelineClips(QVector roles); std::map> m_registeredClips; std::map> m_timelineProducers; -private slots: - void updateFfmpegProgress(); - signals: - void producerChanged(const QString& , const std::shared_ptr &); + void producerChanged(const QString &, const std::shared_ptr &); void gotAudioData(); void refreshPropertiesPanel(); void refreshAnalysisPanel(); void refreshClipDisplay(); void thumbReady(int, const QImage &); - void updateJobStatus(int jobType, int status, int progress = 0, const QString &statusMessage = QString()); /** @brief Clip is ready, load properties. */ void loadPropertiesPanel(); - /** @brief Terminate running audio proxy job. */ - void doAbortAudioThumbs(); - void updateThumbProgress(long); }; #endif diff --git a/src/bin/projectitemmodel.cpp b/src/bin/projectitemmodel.cpp index 199427dae..3c2d773e0 100644 --- a/src/bin/projectitemmodel.cpp +++ b/src/bin/projectitemmodel.cpp @@ -1,652 +1,719 @@ /* 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 "projectitemmodel.h" #include "abstractprojectitem.h" #include "binplaylist.hpp" #include "core.h" #include "doc/kdenlivedoc.h" +#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 "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_nextId(1) , m_blankThumb() { - KImageCache::deleteCache(QStringLiteral("kdenlive-thumbs")); - m_pixmapCache.reset(new KImageCache(QStringLiteral("kdenlive-thumbs"), 10000000)); - m_pixmapCache->setEvictionPolicy(KSharedDataCache::EvictOldest); - QPixmap pix(QSize(160, 90)); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); - } std::shared_ptr ProjectItemModel::construct(QObject *parent) { std::shared_ptr self(new ProjectItemModel(parent)); self->rootItem = ProjectFolder::construct(self); return self; } -ProjectItemModel::~ProjectItemModel() -{ -} +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(';')); 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(';')); - emit addClipCut(list.at(0), list.at(1).toInt(), list.at(2).toInt()); + QString id; + return requestAddBinSubClip(id, list.at(1).toInt(), list.at(2).toInt(), 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 << item->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) { auto tItem = std::static_pointer_cast(item); auto ptr = tItem->parentItem().lock(); if (ptr) { auto index = getIndexFromItem(tItem); emit dataChanged(index, index); } } +void ProjectItemModel::onItemUpdated(const QString &binId) +{ + onItemUpdated(getItemByBinId(binId)); +} 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; } 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; }; for (const auto &child : toDelete) { requestBinClipDeletion(child, undo, redo); } Q_ASSERT(rootItem->childCount() == 0); m_nextId = 1; - m_pixmapCache->clear(); } std::shared_ptr ProjectItemModel::getRootFolder() const { return std::static_pointer_cast(rootItem); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap) { std::shared_ptr clip = getClipByBinID(id); if (!clip) { return; } QMapIterator i(dataMap); QList missingThumbs; int maxFrame = clip->duration().frames(pCore->getCurrentFps()) - 1; + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; 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); } - missingThumbs << in; - // TODO remove access to doc here - auto self = std::static_pointer_cast(shared_from_this()); - ProjectSubClip::construct(clip, self, in, out, pCore->currentDoc()->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode()), - i.key()); - } - if (!missingThumbs.isEmpty()) { - // generate missing subclip thumbnails - clip->slotExtractImage(missingThumbs); + + QString subId; + requestAddBinSubClip(subId, in, out, 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) { 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); 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); } 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); } - int ProjectItemModel::getFreeFolderId() { return m_nextId++; } int ProjectItemModel::getFreeClipId() { return m_nextId++; } bool ProjectItemModel::addItem(std::shared_ptr item, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); - std::shared_ptr parentFolder = getFolderByBinId(parentId); - if (!parentFolder) { + std::shared_ptr parentItem = getItemByBinId(parentId); + if (!parentItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR IN PARENT FOLDER"; return false; } - Fun operation = addItem_lambda(item, parentFolder->getId()); + 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"< new_clip = ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast(shared_from_this())); + Q_ASSERT(!id.isEmpty() && isIdFree(id)); + qDebug() << "/////////// found id"< 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 "<currentDoc()->getFileProperties(description, new_clip->clipId(), 150, true); + int loadJob = pCore->jobManager()->startJob({id}, {}, QString(), description); + pCore->jobManager()->startJob({id}, {loadJob}, QString(), 150, -1, 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("Rename Folder") : undoText); + 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); + return res; +} + +bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, 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); + bool res = addItem(new_clip, parentId, undo, redo); + if (res) { + auto parentJobs = pCore->jobManager()->getPendingJobsIds(parentId, AbstractClipJob::LOADJOB); + pCore->jobManager()->startJob({id}, parentJobs, QString(), 150, -1, true); + } + return res; +} +bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &parentId) +{ + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + bool res = requestAddBinSubClip(id, in, out, 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; } // For correct propagation of the name change, we remove folder from parent first auto parent = currentFolder->parent(); parent->removeChild(currentFolder); currentFolder->setName(newName); // Reinsert in parent return parent->appendChild(currentFolder); }; } 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; }; 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); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } 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) +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> 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; } // In case there are some non-existant parent, we fall back to root - for (const auto & f : downLinks) { + for (const auto &f : downLinks) { if (downLinks.count(upLinks[f.first]) == 0) { - qDebug() << "Warning: parent folder " << upLinks[f.first] << "for folder"< 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 +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; } diff --git a/src/bin/projectitemmodel.h b/src/bin/projectitemmodel.h index 1a957667b..c18021b5d 100644 --- a/src/bin/projectitemmodel.h +++ b/src/bin/projectitemmodel.h @@ -1,206 +1,216 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle 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 PROJECTITEMMODEL_H #define PROJECTITEMMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "mltcontroller/bincontroller.h" -#include "project/jobs/abstractclipjob.h" #include "undohelper.hpp" #include #include -#include class AbstractProjectItem; class BinPlaylist; class ProjectClip; class ProjectFolder; /** * @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 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); /* @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); + bool loadFolders(Mlt::Properties &folders); /** @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); /* @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 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 &parentId, Fun &undo, Fun &redo); + bool requestAddBinSubClip(QString &id, int in, int out, 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(); + 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); - std::unique_ptr m_pixmapCache; public slots: /** @brief An item in the list was modified, notify */ void onItemUpdated(std::shared_ptr item); - + void onItemUpdated(const QString &binId); /** @brief Check whether a given id is currently used or not*/ - bool isIdFree(const QString& id) const; + 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; int m_nextId; QIcon m_blankThumb; signals: - void discardJobs(const QString &id, AbstractClipJob::JOBTYPE type); - void startJob(const QString &id, AbstractClipJob::JOBTYPE type); + // 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 updateThumbProgress(long ms); - void abortAudioThumb(const QString &id, long duration); - void refreshAudioThumbs(const QString &id); - void reloadProducer(const QString &id, const QDomElement &xml); 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/bin/projectsubclip.cpp b/src/bin/projectsubclip.cpp index 178410a87..e8753bc67 100644 --- a/src/bin/projectsubclip.cpp +++ b/src/bin/projectsubclip.cpp @@ -1,162 +1,163 @@ /* Copyright (C) 2015 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 "projectsubclip.h" #include "projectclip.h" #include "projectitemmodel.h" #include #include #include class ClipController; -ProjectSubClip::ProjectSubClip(const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, +ProjectSubClip::ProjectSubClip(const QString &id, const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, const QString &timecode, const QString &name) - : AbstractProjectItem(AbstractProjectItem::SubClipItem, parent->AbstractProjectItem::clipId(), model) + : AbstractProjectItem(AbstractProjectItem::SubClipItem, id, model) , m_masterClip(parent) , m_in(in) , m_out(out) { QPixmap pix(64, 36); pix.fill(Qt::lightGray); m_thumbnail = QIcon(pix); if (name.isEmpty()) { m_name = i18n("Zone %1", parent->childCount() + 1); } else { m_name = name; } m_clipStatus = StatusReady; changeParent(parent); m_duration = timecode; // Save subclip in MLT parent->setProducerProperty("kdenlive:clipzone." + m_name, QString::number(in) + QLatin1Char(';') + QString::number(out)); connect(parent.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb); } -std::shared_ptr ProjectSubClip::construct(std::shared_ptr parent, std::shared_ptr model, int in, int out, - const QString &timecode, const QString &name) +std::shared_ptr ProjectSubClip::construct(const QString &id, std::shared_ptr parent, std::shared_ptr model, + int in, int out, const QString &timecode, const QString &name) { - std::shared_ptr self(new ProjectSubClip(parent, std::move(model), in, out, timecode, name)); - parent->appendChild(self); + std::shared_ptr self(new ProjectSubClip(id, parent, std::move(model), in, out, timecode, name)); baseFinishConstruct(self); return self; } ProjectSubClip::~ProjectSubClip() { // controller is deleted in bincontroller } void ProjectSubClip::gotThumb(int pos, const QImage &img) { if (pos == m_in) { setThumbnail(img); disconnect(m_masterClip.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb); } } void ProjectSubClip::discard() { if (m_masterClip) { m_masterClip->resetProducerProperty("kdenlive:clipzone." + m_name); } } QString ProjectSubClip::getToolTip() const { return QStringLiteral("test"); } std::shared_ptr ProjectSubClip::clip(const QString &id) { Q_UNUSED(id); return std::shared_ptr(); } std::shared_ptr ProjectSubClip::folder(const QString &id) { Q_UNUSED(id); return std::shared_ptr(); } -void ProjectSubClip::setBinEffectsEnabled(bool) -{ -} +void ProjectSubClip::setBinEffectsEnabled(bool) {} GenTime ProjectSubClip::duration() const { // TODO return GenTime(); } QPoint ProjectSubClip::zone() const { return QPoint(m_in, m_out); } std::shared_ptr ProjectSubClip::clipAt(int ix) { Q_UNUSED(ix); return std::shared_ptr(); } QDomElement ProjectSubClip::toXml(QDomDocument &document, bool) { QDomElement sub = document.createElement(QStringLiteral("subclip")); sub.setAttribute(QStringLiteral("id"), m_masterClip->AbstractProjectItem::clipId()); sub.setAttribute(QStringLiteral("in"), m_in); sub.setAttribute(QStringLiteral("out"), m_out); return sub; } std::shared_ptr ProjectSubClip::subClip(int in, int out) { if (m_in == in && m_out == out) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } void ProjectSubClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); m_thumbnail = QIcon(thumb); - if (auto ptr = m_model.lock()) - std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this())); + if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this())); +} + +QPixmap ProjectSubClip::thumbnail(int width, int height) +{ + return m_thumbnail.pixmap(width, height); } bool ProjectSubClip::rename(const QString &name, int column) { // TODO refac: rework this Q_UNUSED(column) if (m_name == name) { return false; } // Rename folder - //if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->bin()->renameSubClipCommand(m_binId, name, m_name, m_in, m_out); + // if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->bin()->renameSubClipCommand(m_binId, name, m_name, m_in, m_out); return true; } std::shared_ptr ProjectSubClip::getMasterClip() const { return m_masterClip; } diff --git a/src/bin/projectsubclip.h b/src/bin/projectsubclip.h index 0f6060a82..5cd79e391 100644 --- a/src/bin/projectsubclip.h +++ b/src/bin/projectsubclip.h @@ -1,93 +1,94 @@ /* Copyright (C) 2015 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 PROJECTSUBCLIP_H #define PROJECTSUBCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include class ProjectFolder; class ProjectClip; class QDomElement; namespace Mlt { } /** * @class ProjectSubClip * @brief Represents a clip in the project (not timeline). * */ class ProjectSubClip : public AbstractProjectItem { Q_OBJECT public: /** * @brief Constructor; used when loading a project and the producer is already available. */ - static std::shared_ptr construct(std::shared_ptr parent, std::shared_ptr model, int in, int out, - const QString &timecode, const QString &name = QString()); + static std::shared_ptr construct(const QString &id, std::shared_ptr parent, std::shared_ptr model, int in, + int out, const QString &timecode, const QString &name = QString()); protected: - ProjectSubClip(const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, const QString &timecode, - const QString &name = QString()); + ProjectSubClip(const QString &id, const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, + const QString &timecode, const QString &name = QString()); public: virtual ~ProjectSubClip(); std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr subClip(int in, int out); std::shared_ptr clipAt(int ix) override; /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; /** @brief Returns the clip's duration. */ GenTime duration() const; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); + QPixmap thumbnail(int width, int height); /** @brief Remove reference to this subclip in the master clip, to be done before a subclip is deleted. */ void discard(); QPoint zone() const override; QString getToolTip() const override; bool rename(const QString &name, int column) override; /** @brief returns a pointer to the parent clip */ std::shared_ptr getMasterClip() const; private: std::shared_ptr m_masterClip; int m_in; int m_out; private slots: void gotThumb(int pos, const QImage &img); }; #endif diff --git a/src/core.cpp b/src/core.cpp index ced352088..918417cc7 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,551 +1,542 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" +#include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/bincontroller.h" -#include "mltcontroller/producerqueue.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" -#include "timeline2/view/timelinewidget.h" -#include "timeline2/view/timelinecontroller.h" #include "timeline2/model/timelineitemmodel.hpp" +#include "timeline2/view/timelinecontroller.h" +#include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() : m_mainWindow(nullptr) , m_projectManager(nullptr) , m_monitorManager(nullptr) - , m_producerQueue(nullptr) , m_binWidget(nullptr) , m_library(nullptr) { } void Core::prepareShutdown() { m_guiConstructed = false; } Core::~Core() { if (m_monitorManager) { delete m_monitorManager; } m_binController->destroyBin(); - //delete m_binWidget; + // delete m_binWidget; delete m_projectManager; } void Core::build(const QString &MltPath) { if (m_self) { qDebug() << "DEBUG: Warning : trying to create a second Core"; return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); // Open connection with Mlt MltConnection::construct(MltPath); // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); } void Core::initGUI(const QUrl &Url) { m_guiConstructed = true; m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = "dv_pal"; } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel); m_binController = std::make_shared(); m_library = new LibraryWidget(m_projectManager); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); connect(m_binWidget, SIGNAL(storeFolder(QString, QString, QString, QString)), m_binController.get(), SLOT(slotStoreFolder(QString, QString, QString, QString))); - connect(m_binController.get(), &BinController::slotProducerReady, m_binWidget, &Bin::slotProducerReady, Qt::DirectConnection); - connect(m_binController.get(), &BinController::prepareTimelineReplacement, m_binWidget, &Bin::prepareTimelineReplacement, Qt::DirectConnection); + // connect(m_binController.get(), &BinController::slotProducerReady, m_binWidget, &Bin::slotProducerReady, Qt::DirectConnection); + // connect(m_binController.get(), &BinController::prepareTimelineReplacement, m_binWidget, &Bin::prepareTimelineReplacement, Qt::DirectConnection); - connect(m_binController.get(), &BinController::requestAudioThumb, m_binWidget, &Bin::slotCreateAudioThumb); + // connect(m_binController.get(), &BinController::requestAudioThumb, m_binWidget, &Bin::slotCreateAudioThumb); connect(m_binController.get(), &BinController::abortAudioThumbs, m_binWidget, &Bin::abortAudioThumbs); connect(m_binController.get(), SIGNAL(loadThumb(QString, QImage, bool)), m_binWidget, SLOT(slotThumbnailReady(QString, QImage, bool))); connect(m_binController.get(), &BinController::setDocumentNotes, m_projectManager, &ProjectManager::setDocumentNotes); m_monitorManager = new MonitorManager(this); + m_jobManager.reset(new JobManager()); // Producer queue, creating MLT::Producers on request + /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO - /*connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ + connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, QString()); QTimer::singleShot(0, pCore->projectManager(), &ProjectManager::slotLoadOnOpen); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } m_mainWindow->show(); } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } std::shared_ptr Core::binController() { return m_binController; } Bin *Core::bin() { return m_binWidget; } -ProducerQueue *Core::producerQueue() +std::shared_ptr Core::jobManager() { - return m_producerQueue; + return m_jobManager; } LibraryWidget *Core::library() { return m_library; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; // inform render widget m_mainWindow->updateRenderWidgetProfile(); return true; } return false; } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { return QSize((int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()); } QSize Core::getCurrentFrameSize() const { return QSize(getCurrentProfile()->width(), getCurrentProfile()->height()); } void Core::requestMonitorRefresh() { - if (!m_guiConstructed) - return; + if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { - if (!m_guiConstructed) - return; + if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemIn(const ObjectId &id) { - if (!m_guiConstructed) - return 0; - switch(id.first) { + if (!m_guiConstructed) return 0; + switch (id.first) { case ObjectType::TimelineClip: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemDuration(const ObjectId &id) { - if (!m_guiConstructed) - return 0; - switch(id.first) { + if (!m_guiConstructed) return 0; + switch (id.first) { case ObjectType::TimelineClip: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { - if (!m_guiConstructed) - return 0; - switch(id.first) { + if (!m_guiConstructed) return 0; + switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { - if (!m_guiConstructed) - return; - switch(id.first) { + if (!m_guiConstructed) return; + switch (id.first) { case ObjectType::TimelineClip: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: - if(m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { + if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->refreshClipMonitor(); break; default: qDebug() << "ERROR: unhandled object type"; } } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } void Core::displayMessage(const QString &message, MessageType type, int timeout) { m_mainWindow->displayMessage(message, type, timeout); } void Core::clearAssetPanel(int itemId) { - if (m_guiConstructed) - m_mainWindow->clearAssetPanel(itemId); + if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } void Core::adjustAssetRange(int itemId, int in, int out) { m_mainWindow->adjustAssetPanelRange(itemId, in, out); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { - if (!m_guiConstructed) - return nullptr; + if (!m_guiConstructed) return nullptr; switch (itemType) { - case (int) ObjectType::TimelineClip: - return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); - case (int) ObjectType::TimelineTrack: - //TODO - return nullptr; - break; - case (int) ObjectType::BinClip: - return m_binWidget->getClipEffectStack(itemId); - default: - return nullptr; + case (int)ObjectType::TimelineClip: + return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); + case (int)ObjectType::TimelineTrack: + // TODO + return nullptr; + break; + case (int)ObjectType::BinClip: + return m_binWidget->getClipEffectStack(itemId); + default: + return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getVideoTrackNames() { - if (!m_guiConstructed) - return QMap(); + if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(true); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return QPair(); return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { - if (!m_guiConstructed) - return; + if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateItem(ObjectId itemId) { if (!m_mainWindow) return; switch (itemId.first) { - case ObjectType::TimelineClip: - m_mainWindow->getCurrentTimeline()->controller()->invalidateClip(itemId.second); - break; - case ObjectType::TimelineTrack: - //TODO: invalidate all clips in track - break; - default: - // bin clip should automatically be reloaded, compositions should not have effects - break; + case ObjectType::TimelineClip: + m_mainWindow->getCurrentTimeline()->controller()->invalidateClip(itemId.second); + break; + case ObjectType::TimelineTrack: + // TODO: invalidate all clips in track + break; + default: + // bin clip should automatically be reloaded, compositions should not have effects + break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } diff --git a/src/core.h b/src/core.h index 2a43ff5b8..653f65e03 100644 --- a/src/core.h +++ b/src/core.h @@ -1,191 +1,191 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #ifndef CORE_H #define CORE_H #include "definitions.h" #include "kdenlivecore_export.h" #include "undohelper.hpp" #include #include #include #include class Bin; class BinController; class DocUndoStack; class EffectStackModel; +class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class Monitor; class MonitorManager; -class ProducerQueue; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; } #define EXIT_RESTART (42) #define pCore Core::self() /** * @class Core * @brief Singleton that provides access to the different parts of Kdenlive * * Needs to be initialize before any widgets are created in MainWindow. * Plugins should be loaded after the widget setup. */ class /*KDENLIVECORE_EXPORT*/ Core : public QObject { Q_OBJECT public: Core(const Core &) = delete; Core &operator=(const Core &) = delete; Core(Core &&) = delete; Core &operator=(Core &&) = delete; virtual ~Core(); /** * @brief Setup the basics of the application, in particular the connection * with Mlt * @param MltPath (optional) path to MLT environment */ static void build(const QString &MltPath = QString()); /** * @brief Init the GUI part of the app and show the main window * @param Url (optional) file to open * If Url is present, it will be opened, otherwhise, if openlastproject is * set, latest project will be opened. If no file is open after trying this, * a default new file will be created. */ void initGUI(const QUrl &Url); /** @brief Returns a pointer to the singleton object. */ static std::unique_ptr &self(); /** @brief Returns a pointer to the main window. */ MainWindow *window(); /** @brief Returns a pointer to the project manager. */ ProjectManager *projectManager(); /** @brief Returns a pointer to the current project. */ KdenliveDoc *currentDoc(); /** @brief Returns a pointer to the monitor manager. */ MonitorManager *monitorManager(); /** @brief Returns a pointer to the project bin controller. */ std::shared_ptr binController(); /** @brief Returns a pointer to the view of the project bin. */ Bin *bin(); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); - /** @brief Returns a pointer to the producer queue. */ - ProducerQueue *producerQueue(); + /** @brief Returns a pointer to the job manager. Please do not store it. */ + std::shared_ptr jobManager(); /** @brief Returns a pointer to the library. */ LibraryWidget *library(); /** @brief Returns a pointer to MLT's repository */ std::unique_ptr &getMltRepository(); /** @brief Returns a pointer to the current profile */ std::unique_ptr &getCurrentProfile() const; const QString &getCurrentProfilePath() const; /** @brief Define the active profile * @returns true if profile exists, false if not found */ bool setCurrentProfile(const QString &profilePath); /** @brief Returns Sample Aspect Ratio of current profile */ double getCurrentSar() const; /** @brief Returns Display Aspect Ratio of current profile */ double getCurrentDar() const; /** @brief Returns frame rate of current profile */ double getCurrentFps() const; /** @brief Returns the frame size (width x height) of current profile */ QSize getCurrentFrameSize() const; /** @brief Returns the frame display size (width x height) of current profile */ QSize getCurrentFrameDisplaySize() const; /** @brief Request project monitor refresh */ void requestMonitorRefresh(); /** @brief Request project monitor refresh if current position is inside range*/ void refreshProjectRange(QSize range); /** @brief Request project monitor refresh if referenced item is under cursor */ void refreshProjectItem(const ObjectId &id); /** @brief Returns a reference to a monitor (clip or project monitor) */ Monitor *getMonitor(int id); /** @brief This function must be called whenever the profile used changes */ void profileChanged(); /** @brief Create and push and undo object based on the corresponding functions Note that if you class permits and requires it, you should use the macro PUSH_UNDO instead*/ void pushUndo(const Fun &undo, const Fun &redo, const QString &text); void pushUndo(QUndoCommand *command); /** @brief display a user info/warning message in statusbar */ void displayMessage(const QString &message, MessageType type, int timeout = -1); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId); void adjustAssetRange(int itemId, int in, int out); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Get a list of video track names with indexes */ QMap getVideoTrackNames(); /** @brief Returns the composition A track (MLT index / Track id) */ QPairgetCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); std::shared_ptr undoStack(); double getClipSpeed(int id) const; void invalidateItem(ObjectId itemId); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); private: explicit Core(); static std::unique_ptr m_self; /** @brief Makes sure Qt's locale and system locale settings match. */ void initLocale(); MainWindow *m_mainWindow; ProjectManager *m_projectManager; MonitorManager *m_monitorManager; std::shared_ptr m_binController; std::shared_ptr m_projectItemModel; - ProducerQueue *m_producerQueue; + std::shared_ptr m_jobManager; Bin *m_binWidget; LibraryWidget *m_library; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; bool m_guiConstructed = false; signals: void coreIsReady(); void updateLibraryPath(); }; #endif diff --git a/src/definitions.h b/src/definitions.h index 29dc84dae..0287262c4 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -1,324 +1,323 @@ /*************************************************************************** * Copyright (C) 2007 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 DEFINITIONS_H #define DEFINITIONS_H #include "effectslist/effectslist.h" #include "gentime.h" #include "kdenlive_debug.h" #include #include #include #include #include #include const int MAXCLIPDURATION = 15000; namespace Kdenlive { enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 }; const int DefaultThumbHeight = 100; } // namespace Kdenlive enum class GroupType { Normal, Selection, // in that case, the group is used to emulate a selection AVSplit, // in that case, the group links the audio and video of the same clip Leaf // This is a leaf (clip or composition) }; const QString groupTypeToStr(GroupType t); GroupType groupTypeFromStr(const QString &s); enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, NoItem }; using ObjectId = std::pair; enum OperationType { None = 0, WaitingForConfirm, MoveOperation, ResizeStart, ResizeEnd, RollingStart, RollingEnd, RippleStart, RippleEnd, FadeIn, FadeOut, TransitionStart, TransitionEnd, MoveGuide, KeyFrame, Seek, Spacer, RubberSelection, ScrollTimeline, ZoomTimeline }; namespace PlaylistState { enum ClipState { Original = 0, VideoOnly = 1, AudioOnly = 2, Disabled = 3 }; } namespace TimelineMode { enum EditMode { NormalEdit = 0, OverwriteEdit = 1, InsertEdit = 2 }; } -enum ClipType { +enum class ClipType { Unknown = 0, Audio = 1, Video = 2, AV = 3, Color = 4, Image = 5, Text = 6, SlideShow = 7, Virtual = 8, Playlist = 9, WebVfx = 10, TextTemplate = 11, QText }; enum ProjectItemType { ProjectClipType = QTreeWidgetItem::UserType, ProjectFoldeType, ProjectSubclipType }; enum GraphicsRectItem { AVWidget = 70000, LabelWidget, TransitionWidget, GroupWidget }; enum ProjectTool { SelectTool = 0, RazorTool = 1, SpacerTool = 2 }; enum MonitorSceneType { MonitorSceneNone = 0, MonitorSceneDefault, MonitorSceneGeometry, MonitorSceneCorners, MonitorSceneRoto, MonitorSceneSplit, MonitorSceneRipple }; enum MessageType { DefaultMessage, ProcessingJobMessage, OperationCompletedMessage, InformationMessage, ErrorMessage, MltError }; enum TrackType { AudioTrack = 0, VideoTrack = 1 }; -enum ClipJobStatus { NoJob = 0, JobWaiting = -1, JobWorking = -2, JobDone = -3, JobCrashed = -4, JobAborted = -5 }; enum CacheType { SystemCacheRoot = -1, CacheRoot = 0, CacheBase = 1, CachePreview = 2, CacheProxy = 3, CacheAudio = 4, CacheThumbs = 5 }; enum TrimMode { NormalTrim, RippleTrim, RollingTrim, SlipTrim, SlideTrim }; class TrackInfo { public: TrackType type; QString trackName; bool isMute; bool isBlind; bool isLocked; int duration; EffectsList effectsList; TrackInfo() : type(VideoTrack) , isMute(0) , isBlind(0) , isLocked(0) , duration(0) , effectsList(true) { } }; struct requestClipInfo { QDomElement xml; QString clipId; int imageHeight; bool replaceProducer; bool operator==(const requestClipInfo &a) const { return clipId == a.clipId; } }; typedef QMap stringMap; typedef QMap> audioByteArray; typedef QVector audioShortVector; class ItemInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** cropStart is the position where the sub-clip starts, relative to the clip's 0 position */ GenTime cropStart; /** cropDuration is the duration of the clip */ GenTime cropDuration; /** Track number */ int track; ItemInfo() : track(0) { } bool isValid() const { return startPos != endPos; } bool contains(GenTime pos) const { if (startPos == endPos) { return true; } return (pos <= endPos && pos >= startPos); } bool operator==(const ItemInfo &a) const { return startPos == a.startPos && endPos == a.endPos && track == a.track && cropStart == a.cropStart; } }; class TransitionInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** the track on which the transition is (b_track)*/ int b_track; /** the track on which the transition is applied (a_track)*/ int a_track; /** Does the user request for a special a_track */ bool forceTrack; TransitionInfo() : b_track(0) , a_track(0) , forceTrack(0) { } }; class CommentedTime { public: CommentedTime(); CommentedTime(const GenTime &time, const QString &comment, int markerType = 0); CommentedTime(const QString &hash, const GenTime &time); QString comment() const; GenTime time() const; /** @brief Returns a string containing infos needed to store marker info. string equals marker type + QLatin1Char(':') + marker comment */ QString hash() const; void setComment(const QString &comm); void setMarkerType(int t); int markerType() const; static QColor markerColor(int type); /* Implementation of > operator; Works identically as with basic types. */ bool operator>(const CommentedTime &op) const; /* Implementation of < operator; Works identically as with basic types. */ bool operator<(const CommentedTime &op) const; /* Implementation of >= operator; Works identically as with basic types. */ bool operator>=(const CommentedTime &op) const; /* Implementation of <= operator; Works identically as with basic types. */ bool operator<=(const CommentedTime &op) const; /* Implementation of == operator; Works identically as with basic types. */ bool operator==(const CommentedTime &op) const; /* Implementation of != operator; Works identically as with basic types. */ bool operator!=(const CommentedTime &op) const; private: GenTime m_time; QString m_comment; int m_type; }; QDebug operator<<(QDebug qd, const ItemInfo &info); // we provide hash function for qstring and QPersistentModelIndex namespace std { template <> struct hash { std::size_t operator()(const QString &k) const { return qHash(k); } }; template <> struct hash { std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); } }; } // namespace std // The following is a hack that allows to use shared_from_this in the case of a multiple inheritance. // Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance template struct enable_shared_from_this_virtual; class enable_shared_from_this_virtual_base : public std::enable_shared_from_this { typedef std::enable_shared_from_this base_type; template friend struct enable_shared_from_this_virtual; std::shared_ptr shared_from_this() { return base_type::shared_from_this(); } std::shared_ptr shared_from_this() const { return base_type::shared_from_this(); } }; template struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base { typedef enable_shared_from_this_virtual_base base_type; public: std::shared_ptr shared_from_this() { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } std::shared_ptr shared_from_this() const { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } }; // This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance // Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object template class QAbstractItemModel_shared_from_this : public QAbstractItemModel { protected: QAbstractItemModel_shared_from_this() : QAbstractItemModel() { } public: std::shared_ptr shared_from_this() { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } std::shared_ptr shared_from_this() const { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } public: // actually private, but avoids compiler template friendship issues mutable std::weak_ptr weak_this_; }; #endif diff --git a/src/dialogs/clipcreationdialog.cpp b/src/dialogs/clipcreationdialog.cpp index 521fb8efb..70d0ebc2c 100644 --- a/src/dialogs/clipcreationdialog.cpp +++ b/src/dialogs/clipcreationdialog.cpp @@ -1,597 +1,456 @@ /* Copyright (C) 2015 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 "clipcreationdialog.h" #include "bin/bin.h" #include "bin/bincommands.h" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" +#include "bin/projectitemmodel.h" #include "core.h" +#include "definitions.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "project/dialogs/slideshowclip.h" #include "timecodedisplay.h" #include "titler/titlewidget.h" #include "titletemplatedialog.h" #include "ui_colorclip_ui.h" #include "ui_qtextclip_ui.h" #include "utils/devices.hpp" #include "xml/xml.hpp" #include "klocalizedstring.h" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include +#include // static QStringList ClipCreationDialog::getExtensions() { // Build list of MIME types - QStringList mimeTypes = QStringList() << QStringLiteral("application/x-kdenlive") << QStringLiteral("application/x-kdenlivetitle") << QStringLiteral("video/mlt-playlist") << QStringLiteral("text/plain"); + QStringList mimeTypes = QStringList() << QStringLiteral("") << QStringLiteral("application/x-kdenlivetitle") << QStringLiteral("video/mlt-playlist") + << QStringLiteral("text/plain"); // Video MIMEs - mimeTypes << QStringLiteral("video/x-flv") << QStringLiteral("application/vnd.rn-realmedia") << QStringLiteral("video/x-dv") << QStringLiteral("video/dv") << QStringLiteral("video/x-msvideo") << QStringLiteral("video/x-matroska") << QStringLiteral("video/mpeg") << QStringLiteral("video/ogg") << QStringLiteral("video/x-ms-wmv") << QStringLiteral("video/mp4") << QStringLiteral("video/quicktime") << QStringLiteral("video/webm") << QStringLiteral("video/3gpp") << QStringLiteral("video/mp2t"); + mimeTypes << QStringLiteral("video/x-flv") << QStringLiteral("application/vnd.rn-realmedia") << QStringLiteral("video/x-dv") << QStringLiteral("video/dv") + << QStringLiteral("video/x-msvideo") << QStringLiteral("video/x-matroska") << QStringLiteral("video/mpeg") << QStringLiteral("video/ogg") + << QStringLiteral("video/x-ms-wmv") << QStringLiteral("video/mp4") << QStringLiteral("video/quicktime") << QStringLiteral("video/webm") + << QStringLiteral("video/3gpp") << QStringLiteral("video/mp2t"); // Audio MIMEs - mimeTypes << QStringLiteral("audio/x-flac") << QStringLiteral("audio/x-matroska") << QStringLiteral("audio/mp4") << QStringLiteral("audio/mpeg") << QStringLiteral("audio/x-mp3") << QStringLiteral("audio/ogg") << QStringLiteral("audio/x-wav") << QStringLiteral("audio/x-aiff") << QStringLiteral("audio/aiff") << QStringLiteral("application/ogg") << QStringLiteral("application/mxf") << QStringLiteral("application/x-shockwave-flash") << QStringLiteral("audio/ac3"); + mimeTypes << QStringLiteral("audio/x-flac") << QStringLiteral("audio/x-matroska") << QStringLiteral("audio/mp4") << QStringLiteral("audio/mpeg") + << QStringLiteral("audio/x-mp3") << QStringLiteral("audio/ogg") << QStringLiteral("audio/x-wav") << QStringLiteral("audio/x-aiff") + << QStringLiteral("audio/aiff") << QStringLiteral("application/ogg") << QStringLiteral("application/mxf") + << QStringLiteral("application/x-shockwave-flash") << QStringLiteral("audio/ac3"); // Image MIMEs - mimeTypes << QStringLiteral("image/gif") << QStringLiteral("image/jpeg") << QStringLiteral("image/png") << QStringLiteral("image/x-tga") << QStringLiteral("image/x-bmp") << QStringLiteral("image/svg+xml") << QStringLiteral("image/tiff") << QStringLiteral("image/x-xcf") << QStringLiteral("image/x-xcf-gimp") << QStringLiteral("image/x-vnd.adobe.photoshop") << QStringLiteral("image/x-pcx") << QStringLiteral("image/x-exr") << QStringLiteral("image/x-portable-pixmap") << QStringLiteral("application/x-krita"); + mimeTypes << QStringLiteral("image/gif") << QStringLiteral("image/jpeg") << QStringLiteral("image/png") << QStringLiteral("image/x-tga") + << QStringLiteral("image/x-bmp") << QStringLiteral("image/svg+xml") << QStringLiteral("image/tiff") << QStringLiteral("image/x-xcf") + << QStringLiteral("image/x-xcf-gimp") << QStringLiteral("image/x-vnd.adobe.photoshop") << QStringLiteral("image/x-pcx") + << QStringLiteral("image/x-exr") << QStringLiteral("image/x-portable-pixmap") << QStringLiteral("application/x-krita"); QMimeDatabase db; QStringList allExtensions; for (const QString &mimeType : mimeTypes) { QMimeType mime = db.mimeTypeForName(mimeType); if (mime.isValid()) { allExtensions.append(mime.globPatterns()); } } // process custom user extensions const QStringList customs = KdenliveSettings::addedExtensions().split(' ', QString::SkipEmptyParts); if (!customs.isEmpty()) { for (const QString &ext : customs) { if (ext.startsWith(QLatin1String("*."))) { allExtensions << ext; } else if (ext.startsWith(QLatin1String("."))) { allExtensions << QStringLiteral("*") + ext; } else if (!ext.contains(QLatin1Char('.'))) { allExtensions << QStringLiteral("*.") + ext; } else { // Unrecognized format qCDebug(KDENLIVE_LOG) << "Unrecognized custom format: " << ext; } } } allExtensions.removeDuplicates(); return allExtensions; } -// static -void ClipCreationDialog::createClipFromXml(KdenliveDoc *doc, QDomElement &xml, const QStringList &groupInfo, Bin *bin) -{ - // FIXME? - Q_UNUSED(groupInfo) - - int id = bin->getFreeClipId(); - xml.setAttribute(QStringLiteral("id"), QString::number(id)); - AddClipCommand *command = new AddClipCommand(bin, xml, QString::number(id), true); - doc->commandStack()->push(command); -} - // static void ClipCreationDialog::createColorClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) { QScopedPointer dia(new QDialog(qApp->activeWindow())); Ui::ColorClip_UI dia_ui; dia_ui.setupUi(dia.data()); dia->setWindowTitle(i18n("Color Clip")); dia_ui.clip_name->setText(i18n("Color Clip")); QScopedPointer t(new TimecodeDisplay(doc->timecode())); t->setValue(KdenliveSettings::color_duration()); dia_ui.clip_durationBox->addWidget(t.data()); dia_ui.clip_color->setColor(KdenliveSettings::colorclipcolor()); if (dia->exec() == QDialog::Accepted) { QString color = dia_ui.clip_color->color().name(); KdenliveSettings::setColorclipcolor(color); color = color.replace(0, 1, QStringLiteral("0x")) + "ff"; - int duration = doc->getFramePos(doc->timecode().getTimecode(t->gentime())); + int duration = doc->getFramePos(doc->timecode().getTimecode(t->gentime())); QString name = dia_ui.clip_name->text(); ClipCreator::createColorClip(color, duration, name, parentFolder, model); } } -void ClipCreationDialog::createQTextClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin, ProjectClip *clip) +void ClipCreationDialog::createQTextClip(KdenliveDoc *doc, const QString &parentId, Bin *bin, ProjectClip *clip) { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); QScopedPointer dia(new QDialog(bin)); Ui::QTextClip_UI dia_ui; dia_ui.setupUi(dia.data()); dia->setWindowTitle(i18n("Text Clip")); dia_ui.fgColor->setAlphaChannelEnabled(true); dia_ui.lineColor->setAlphaChannelEnabled(true); dia_ui.bgColor->setAlphaChannelEnabled(true); if (clip) { dia_ui.name->setText(clip->getProducerProperty(QStringLiteral("kdenlive:clipname"))); dia_ui.text->setPlainText(clip->getProducerProperty(QStringLiteral("text"))); dia_ui.fgColor->setColor(clip->getProducerProperty(QStringLiteral("fgcolour"))); dia_ui.bgColor->setColor(clip->getProducerProperty(QStringLiteral("bgcolour"))); dia_ui.pad->setValue(clip->getProducerProperty(QStringLiteral("pad")).toInt()); dia_ui.lineColor->setColor(clip->getProducerProperty(QStringLiteral("olcolour"))); dia_ui.lineWidth->setValue(clip->getProducerProperty(QStringLiteral("outline")).toInt()); dia_ui.font->setCurrentFont(QFont(clip->getProducerProperty(QStringLiteral("family")))); dia_ui.fontSize->setValue(clip->getProducerProperty(QStringLiteral("size")).toInt()); dia_ui.weight->setValue(clip->getProducerProperty(QStringLiteral("weight")).toInt()); dia_ui.italic->setChecked(clip->getProducerProperty(QStringLiteral("style")) == QStringLiteral("italic")); dia_ui.duration->setText(doc->timecode().getTimecodeFromFrames(clip->getProducerProperty(QStringLiteral("out")).toInt())); } else { dia_ui.name->setText(i18n("Text Clip")); dia_ui.fgColor->setColor(titleConfig.readEntry(QStringLiteral("font_color"))); dia_ui.bgColor->setColor(titleConfig.readEntry(QStringLiteral("background_color"))); dia_ui.lineColor->setColor(titleConfig.readEntry(QStringLiteral("font_outline_color"))); dia_ui.lineWidth->setValue(titleConfig.readEntry(QStringLiteral("font_outline")).toInt()); dia_ui.font->setCurrentFont(QFont(titleConfig.readEntry(QStringLiteral("font_family")))); dia_ui.fontSize->setValue(titleConfig.readEntry(QStringLiteral("font_pixel_size")).toInt()); dia_ui.weight->setValue(titleConfig.readEntry(QStringLiteral("font_weight")).toInt()); dia_ui.italic->setChecked(titleConfig.readEntry(QStringLiteral("font_italic")).toInt() != 0); dia_ui.duration->setText(titleConfig.readEntry(QStringLiteral("title_duration"))); } if (dia->exec() == QDialog::Accepted) { // KdenliveSettings::setColorclipcolor(color); QDomDocument xml; QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); - prod.setAttribute(QStringLiteral("type"), (int) QText); + prod.setAttribute(QStringLiteral("type"), (int)ClipType::QText); int id = bin->getFreeClipId(); prod.setAttribute(QStringLiteral("id"), QString::number(id)); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("out"), doc->timecode().getFrameCount(dia_ui.duration->text())); QMap properties; properties.insert(QStringLiteral("kdenlive:clipname"), dia_ui.name->text()); - if (!groupInfo.isEmpty()) { - properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); + if (!parentId.isEmpty()) { + properties.insert(QStringLiteral("kdenlive:folderid"), parentId); } properties.insert(QStringLiteral("mlt_service"), QStringLiteral("qtext")); properties.insert(QStringLiteral("out"), QString::number(doc->timecode().getFrameCount(dia_ui.duration->text()))); properties.insert(QStringLiteral("length"), dia_ui.duration->text()); // properties.insert(QStringLiteral("scale"), QStringLiteral("off")); // properties.insert(QStringLiteral("fill"), QStringLiteral("0")); properties.insert(QStringLiteral("text"), dia_ui.text->document()->toPlainText()); properties.insert(QStringLiteral("fgcolour"), dia_ui.fgColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("bgcolour"), dia_ui.bgColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("olcolour"), dia_ui.lineColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("outline"), QString::number(dia_ui.lineWidth->value())); properties.insert(QStringLiteral("pad"), QString::number(dia_ui.pad->value())); properties.insert(QStringLiteral("family"), dia_ui.font->currentFont().family()); properties.insert(QStringLiteral("size"), QString::number(dia_ui.fontSize->value())); properties.insert(QStringLiteral("style"), dia_ui.italic->isChecked() ? QStringLiteral("italic") : QStringLiteral("normal")); properties.insert(QStringLiteral("weight"), QString::number(dia_ui.weight->value())); if (clip) { QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("length"), clip->getProducerProperty(QStringLiteral("length"))); 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"))); bin->slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } else { Xml::addXmlProperties(prod, properties); - AddClipCommand *command = new AddClipCommand(bin, xml.documentElement(), QString::number(id), true); - doc->commandStack()->push(command); + QString clipId = QString::number(id); + pCore->projectItemModel()->requestAddBinClip(clipId, xml.documentElement(), parentId, i18n("Create Title clip")); } } } // static -void ClipCreationDialog::createSlideshowClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin) +void ClipCreationDialog::createSlideshowClip(KdenliveDoc *doc, const QString &parentId, std::shared_ptr model) { - QPointer dia = new SlideshowClip(doc->timecode(), KRecentDirs::dir(QStringLiteral(":KdenliveSlideShowFolder")), nullptr, bin); + QScopedPointer dia( + new SlideshowClip(doc->timecode(), KRecentDirs::dir(QStringLiteral(":KdenliveSlideShowFolder")), nullptr, QApplication::activeWindow())); if (dia->exec() == QDialog::Accepted) { // Ready, create xml KRecentDirs::add(QStringLiteral(":KdenliveSlideShowFolder"), QUrl::fromLocalFile(dia->selectedPath()).adjusted(QUrl::RemoveFilename).toLocalFile()); - QDomDocument xml; - QDomElement prod = xml.createElement(QStringLiteral("producer")); - xml.appendChild(prod); - prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); - prod.setAttribute(QStringLiteral("out"), QString::number(doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1)); - prod.setAttribute(QStringLiteral("type"), (int)SlideShow); - QMap properties; - properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName()); - properties.insert(QStringLiteral("resource"), dia->selectedPath()); - properties.insert(QStringLiteral("ttl"), QString::number(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(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()); - if (!groupInfo.isEmpty()) { - properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); - } - Xml::addXmlProperties(prod, properties); - int id = bin->getFreeClipId(); - AddClipCommand *command = new AddClipCommand(bin, xml.documentElement(), QString::number(id), true); - doc->commandStack()->push(command); + std::unordered_map properties; + properties[QStringLiteral("ttl")] = QString::number(doc->getFramePos(dia->clipDuration())); + properties[QStringLiteral("loop")] = QString::number(static_cast(dia->loop())); + properties[QStringLiteral("crop")] = QString::number(static_cast(dia->crop())); + properties[QStringLiteral("fade")] = QString::number(static_cast(dia->fade())); + properties[QStringLiteral("luma_duration")] = QString::number(doc->getFramePos(dia->lumaDuration())); + properties[QStringLiteral("luma_file")] = dia->lumaFile(); + properties[QStringLiteral("softness")] = QString::number(dia->softness()); + properties[QStringLiteral("animation")] = dia->animation(); + + int duration = doc->getFramePos(dia->clipDuration()) * dia->imageCount(); + ClipCreator::createSlideshowClip(dia->selectedPath(), duration, dia->clipName(), parentId, properties, model); } - delete dia; } void ClipCreationDialog::createTitleClip(KdenliveDoc *doc, const QStringList &groupInfo, const QString &templatePath, Bin *bin) { // Make sure the titles folder exists QDir dir(doc->projectDataFolder() + QStringLiteral("/titles")); dir.mkpath(QStringLiteral(".")); QPointer dia_ui = new TitleWidget(QUrl::fromLocalFile(templatePath), doc->timecode(), dir.absolutePath(), bin->monitor(), bin); QObject::connect(dia_ui.data(), &TitleWidget::requestBackgroundFrame, bin, &Bin::slotGetCurrentProjectImage); if (dia_ui->exec() == QDialog::Accepted) { // Ready, create clip xml QDomDocument xml; QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); - //prod.setAttribute("resource", imagePath); + // prod.setAttribute("resource", imagePath); int id = bin->getFreeClipId(); prod.setAttribute(QStringLiteral("id"), QString::number(id)); QMap properties; properties.insert(QStringLiteral("xmldata"), dia_ui->xml().toString()); QString titleSuggestion = dia_ui->titleSuggest(); properties.insert(QStringLiteral("kdenlive:clipname"), titleSuggestion.isEmpty() ? i18n("Title clip") : titleSuggestion); if (!groupInfo.isEmpty()) { properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); } Xml::addXmlProperties(prod, properties); - prod.setAttribute(QStringLiteral("type"), (int)Text); + prod.setAttribute(QStringLiteral("type"), (int)ClipType::Text); prod.setAttribute(QStringLiteral("transparency"), QStringLiteral("1")); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("out"), dia_ui->duration() - 1); + // TODO refac + /* AddClipCommand *command = new AddClipCommand(bin, xml.documentElement(), QString::number(id), true); doc->commandStack()->push(command); + */ } delete dia_ui; } -void ClipCreationDialog::createTitleTemplateClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin) +void ClipCreationDialog::createTitleTemplateClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) { - QPointer dia = new TitleTemplateDialog(doc->projectDataFolder(), QApplication::activeWindow()); + QScopedPointer dia(new TitleTemplateDialog(doc->projectDataFolder(), QApplication::activeWindow())); if (dia->exec() == QDialog::Accepted) { - QString textTemplate = dia->selectedTemplate(); - // Create a cloned template clip - QDomDocument xml; - QDomElement prod = xml.createElement(QStringLiteral("producer")); - xml.appendChild(prod); - - QMap properties; - properties.insert(QStringLiteral("resource"), textTemplate); - properties.insert(QStringLiteral("kdenlive:clipname"), i18n("Template title clip")); - if (!groupInfo.isEmpty()) { - properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); - } - Xml::addXmlProperties(prod, properties); - int id = bin->getFreeClipId(); - prod.setAttribute(QStringLiteral("id"), QString::number(id)); - prod.setAttribute(QStringLiteral("type"), (int)TextTemplate); - prod.setAttribute(QStringLiteral("transparency"), QStringLiteral("1")); - prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); - prod.setAttribute(QStringLiteral("templatetext"), dia->selectedText()); - - int duration = 0; - QDomDocument titledoc; - QFile txtfile(textTemplate); - if (txtfile.open(QIODevice::ReadOnly) && titledoc.setContent(&txtfile)) { - if (titledoc.documentElement().hasAttribute(QStringLiteral("duration"))) { - duration = titledoc.documentElement().attribute(QStringLiteral("duration")).toInt(); - } else { - // keep some time for backwards compatibility - 26/12/12 - duration = titledoc.documentElement().attribute(QStringLiteral("out")).toInt(); - } - } - txtfile.close(); - - if (duration == 0) { - duration = doc->getFramePos(KdenliveSettings::title_duration()); - } - prod.setAttribute(QStringLiteral("duration"), duration - 1); - prod.setAttribute(QStringLiteral("out"), duration - 1); - - AddClipCommand *command = new AddClipCommand(bin, xml.documentElement(), QString::number(id), true); - doc->commandStack()->push(command); + ClipCreator::createTitleTemplate(dia->selectedTemplate(), dia->selectedText(), i18n("Template title clip"), parentFolder, model); } - delete dia; } -void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QList &urls, const QStringList &groupInfo, Bin *bin, - const QMap &data) -{ - auto *addClips = new QUndoCommand(); +// void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QList &urls, const QStringList &groupInfo, Bin *bin, +// const QMap &data) +// { +// auto *addClips = new QUndoCommand(); // TODO: check files on removable volume /*listRemovableVolumes(); for (const QUrl &file : urls) { if (QFile::exists(file.path())) { //TODO check for duplicates if (!data.contains("bypassDuplicate") && !getClipByResource(file.path()).empty()) { if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("Clip %1
already exists in project, what do you want to do?", file.path()), i18n("Clip already exists")) == KMessageBox::Cancel) continue; } if (isOnRemovableDevice(file) && !isOnRemovableDevice(m_doc->projectFolder())) { int answer = KMessageBox::warningYesNoCancel(QApplication::activeWindow(), i18n("Clip %1
is on a removable device, will not be available when device is unplugged", file.path()), i18n("File on a Removable Device"), KGuiItem(i18n("Copy file to project folder")), KGuiItem(i18n("Continue")), KStandardGuiItem::cancel(), QString("copyFilesToProjectFolder")); if (answer == KMessageBox::Cancel) continue; else if (answer == KMessageBox::Yes) { // Copy files to project folder QDir sourcesFolder(m_doc->projectFolder().toLocalFile()); sourcesFolder.cd("clips"); KIO::MkdirJob *mkdirJob = KIO::mkdir(QUrl::fromLocalFile(sourcesFolder.absolutePath())); KJobWidgets::setWindow(mkdirJob, QApplication::activeWindow()); if (!mkdirJob->exec()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Cannot create directory %1", sourcesFolder.absolutePath())); continue; } //KIO::filesize_t m_requestedSize; KIO::CopyJob *copyjob = KIO::copy(file, QUrl::fromLocalFile(sourcesFolder.absolutePath())); //TODO: for some reason, passing metadata does not work... copyjob->addMetaData("group", data.value("group")); copyjob->addMetaData("groupId", data.value("groupId")); copyjob->addMetaData("comment", data.value("comment")); KJobWidgets::setWindow(copyjob, QApplication::activeWindow()); connect(copyjob, &KIO::CopyJob::copyingDone, this, &ClipManager::slotAddCopiedClip); continue; } }*/ // TODO check folders /*QList< QList > foldersList; QMimeDatabase db; for (const QUrl & file : list) { // Check there is no folder here QMimeType type = db.mimeTypeForUrl(file); if (type.inherits("inode/directory")) { // user dropped a folder, import its files list.removeAll(file); QDir dir(file.path()); QStringList result = dir.entryList(QDir::Files); QList folderFiles; folderFiles << file; for (const QString & path : result) { // TODO: create folder command folderFiles.append(QUrl::fromLocalFile(dir.absoluteFilePath(path))); } if (folderFiles.count() > 1) foldersList.append(folderFiles); } }*/ +//} - for (const QUrl &file : urls) { - if (isOnRemovableDevice(file) && !isOnRemovableDevice(pCore->currentDoc()->projectDataFolder())) { - int answer = - KMessageBox::messageBox(QApplication::activeWindow(), KMessageBox::DialogType::WarningContinueCancel, - i18n("Clip %1
is on a removable device, will not be available when device is unplugged or mounted at a different position. You may want to copy it first to your hard-drive. Would you like to add it anyways?", file.path()), - i18n("Removable device"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), QStringLiteral("removable")); - - if (answer == KMessageBox::Cancel) continue; - } - QDomDocument xml; - QDomElement prod = xml.createElement(QStringLiteral("producer")); - xml.appendChild(prod); - QMap properties; - properties.insert(QStringLiteral("resource"), file.toLocalFile()); - if (!groupInfo.isEmpty()) { - properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); - } - // Merge data - QMapIterator i(data); - while (i.hasNext()) { - i.next(); - properties.insert(i.key(), i.value()); - } - int id = bin->getFreeClipId(); - prod.setAttribute(QStringLiteral("id"), QString::number(id)); - QMimeDatabase db; - QMimeType type = db.mimeTypeForUrl(file); - if (type.name().startsWith(QLatin1String("image/"))) { - prod.setAttribute(QStringLiteral("type"), (int)Image); - prod.setAttribute(QStringLiteral("in"), 0); - prod.setAttribute(QStringLiteral("length"), doc->getFramePos(KdenliveSettings::image_duration())); - } else if (type.inherits(QStringLiteral("application/x-kdenlivetitle"))) { - // opening a title file - QDomDocument txtdoc(QStringLiteral("titledocument")); - QFile txtfile(file.toLocalFile()); - if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { - txtfile.close(); - prod.setAttribute(QStringLiteral("type"), (int)Text); - // extract embedded images - QDomNodeList items = txtdoc.elementsByTagName(QStringLiteral("content")); - for (int j = 0; j < items.count(); ++j) { - QDomElement content = items.item(j).toElement(); - if (content.hasAttribute(QStringLiteral("base64"))) { - QString titlesFolder = doc->projectDataFolder() + QStringLiteral("/titles/"); - QString path = TitleDocument::extractBase64Image(titlesFolder, content.attribute(QStringLiteral("base64"))); - if (!path.isEmpty()) { - content.setAttribute(QStringLiteral("url"), path); - content.removeAttribute(QStringLiteral("base64")); - } - } - } - prod.setAttribute(QStringLiteral("in"), 0); - int duration = 0; - if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { - duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); - } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { - duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); - } - if (duration <= 0) { - duration = doc->getFramePos(KdenliveSettings::title_duration()) - 1; - } - prod.setAttribute(QStringLiteral("duration"), duration); - prod.setAttribute(QStringLiteral("out"), duration); - txtdoc.documentElement().setAttribute(QStringLiteral("duration"), duration); - txtdoc.documentElement().setAttribute(QStringLiteral("out"), duration); - QString titleData = txtdoc.toString(); - prod.setAttribute(QStringLiteral("xmldata"), titleData); - } else { - txtfile.close(); - } - } - Xml::addXmlProperties(prod, properties); - new AddClipCommand(bin, xml.documentElement(), QString::number(id), true, addClips); - } - // We reset the state of the "don't ask again" for the question about removable devices - KMessageBox::enableMessage(QStringLiteral("removable")); - if (addClips->childCount() > 0) { - addClips->setText(i18np("Add clip", "Add clips", addClips->childCount())); - doc->commandStack()->push(addClips); - } -} - -void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin) +void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) { + qDebug() << "/////////// starting to add bin clips"; QList list; QString allExtensions = getExtensions().join(QLatin1Char(' ')); QString dialogFilter = allExtensions + QLatin1Char('|') + i18n("All Supported Files") + QStringLiteral("\n*|") + i18n("All Files"); QCheckBox *b = new QCheckBox(i18n("Import image sequence")); b->setChecked(KdenliveSettings::autoimagesequence()); QFrame *f = new QFrame(); f->setFrameShape(QFrame::NoFrame); auto *l = new QHBoxLayout; l->addWidget(b); l->addStretch(5); f->setLayout(l); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); if (clipFolder.isEmpty()) { clipFolder = QDir::homePath(); } QScopedPointer dlg(new QDialog((QWidget *)doc->parent())); - QScopedPointer fileWidget (new KFileWidget(QUrl::fromLocalFile(clipFolder), dlg.data())); + QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(clipFolder), dlg.data())); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); fileWidget->setCustomWidget(f); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setFilter(dialogFilter); - fileWidget->setMode(KFile::Files | KFile::ExistingOnly | KFile::LocalOnly); + fileWidget->setMode(KFile::Files | KFile::ExistingOnly | KFile::LocalOnly | KFile::Directory); KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { KdenliveSettings::setAutoimagesequence(b->isChecked()); list = fileWidget->selectedUrls(); if (!list.isEmpty()) { KRecentDirs::add(QStringLiteral(":KdenliveClipFolder"), list.constFirst().adjusted(QUrl::RemoveFilename).toLocalFile()); } if (b->isChecked() && list.count() >= 1) { // Check for image sequence const QUrl &url = list.at(0); QString fileName = url.fileName().section(QLatin1Char('.'), 0, -2); if (fileName.at(fileName.size() - 1).isDigit()) { KFileItem item(url); if (item.mimetype().startsWith(QLatin1String("image"))) { // import as sequence if we found more than one image in the sequence QStringList patternlist; QString pattern = SlideshowClip::selectedPath(url, false, QString(), &patternlist); qCDebug(KDENLIVE_LOG) << " / // IMPORT PATTERN: " << pattern << " COUNT: " << patternlist.count(); int count = patternlist.count(); if (count > 1) { // get image sequence base name while (fileName.at(fileName.size() - 1).isDigit()) { fileName.chop(1); } - QDomDocument xml; - QDomElement prod = xml.createElement(QStringLiteral("producer")); - xml.appendChild(prod); - prod.setAttribute(QStringLiteral("type"), (int)SlideShow); - prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); QString duration = doc->timecode().reformatSeparators(KdenliveSettings::sequence_duration()); - prod.setAttribute(QStringLiteral("out"), QString::number(doc->getFramePos(duration) * count - 1)); - QMap properties; - properties.insert(QStringLiteral("resource"), pattern); - properties.insert(QStringLiteral("kdenlive:clipname"), fileName); - properties.insert(QStringLiteral("ttl"), QString::number(doc->getFramePos(duration))); - properties.insert(QStringLiteral("loop"), QString::number(0)); - properties.insert(QStringLiteral("crop"), QString::number(0)); - properties.insert(QStringLiteral("fade"), QString::number(0)); - properties.insert(QStringLiteral("luma_duration"), - QString::number(doc->getFramePos(doc->timecode().getTimecodeFromFrames(int(ceil(doc->timecode().fps())))))); - if (!groupInfo.isEmpty()) { - properties.insert(QStringLiteral("kdenlive:folderid"), groupInfo.at(0)); - } - Xml::addXmlProperties(prod, properties); - int id = bin->getFreeClipId(); - AddClipCommand *command = new AddClipCommand(bin, xml.documentElement(), QString::number(id), true); - doc->commandStack()->push(command); + std::unordered_map properties; + properties[QStringLiteral("ttl")] = QString::number(doc->getFramePos(duration)); + properties[QStringLiteral("loop")] = QString::number(0); + properties[QStringLiteral("crop")] = QString::number(0); + properties[QStringLiteral("fade")] = QString::number(0); + properties[QStringLiteral("luma_duration")] = + QString::number(doc->getFramePos(doc->timecode().getTimecodeFromFrames(int(ceil(doc->timecode().fps()))))); + int frame_duration = doc->getFramePos(duration) * count; + ClipCreator::createSlideshowClip(pattern, frame_duration, fileName, parentFolder, properties, model); return; } } } } } + qDebug() << "/////////// found list"<group("FileDialogSize"); if (handle) { KWindowConfig::saveWindowSize(handle, group); } + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + bool created = ClipCreator::createClipsFromList(list, true, parentFolder, model, undo, redo); - if (!list.isEmpty()) { - ClipCreationDialog::createClipsCommand(doc, list, groupInfo, bin); + // We reset the state of the "don't ask again" for the question about removable devices + KMessageBox::enableMessage(QStringLiteral("removable")); + if (created) { + pCore->pushUndo(undo, redo, i18np("Add clip", "Add clips", list.size())); } } - -void ClipCreationDialog::createClipsCommand(Bin *bin, const QDomElement &producer, const QString &id, QUndoCommand *command) -{ - new AddClipCommand(bin, producer, id, true, command); -} diff --git a/src/dialogs/clipcreationdialog.h b/src/dialogs/clipcreationdialog.h index 2e2d31864..41276717b 100644 --- a/src/dialogs/clipcreationdialog.h +++ b/src/dialogs/clipcreationdialog.h @@ -1,54 +1,50 @@ /* Copyright (C) 2015 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 CLIPCREATIONDIALOG_H #define CLIPCREATIONDIALOG_H #include "definitions.h" class KdenliveDoc; class QUndoCommand; class Bin; class ProjectClip; class ProjectItemModel; /** * @namespace ClipCreationDialog * @brief This namespace contains a list of static methods displaying widgets * allowing creation of all clip types. */ namespace ClipCreationDialog { QStringList getExtensions(); void createColorClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); -void createQTextClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin, ProjectClip *clip = nullptr); -void createClipFromXml(KdenliveDoc *doc, QDomElement &xml, const QStringList &groupInfo, Bin *bin); -void createSlideshowClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin); +void createQTextClip(KdenliveDoc *doc, const QString &parentId, Bin *bin, ProjectClip *clip = nullptr); +void createSlideshowClip(KdenliveDoc *doc, const QString &parentId, std::shared_ptr model); void createTitleClip(KdenliveDoc *doc, const QStringList &groupInfo, const QString &templatePath, Bin *bin); -void createTitleTemplateClip(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin); -void createClipsCommand(KdenliveDoc *doc, const QList &urls, const QStringList &groupInfo, Bin *bin, - const QMap &data = QMap()); -void createClipsCommand(Bin *bin, const QDomElement &producer, const QString &id, QUndoCommand *command); -void createClipsCommand(KdenliveDoc *doc, const QStringList &groupInfo, Bin *bin); +void createTitleTemplateClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); +void createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); } #endif diff --git a/src/dialogs/markerdialog.cpp b/src/dialogs/markerdialog.cpp index 66b521f67..7dc3bcbff 100644 --- a/src/dialogs/markerdialog.cpp +++ b/src/dialogs/markerdialog.cpp @@ -1,131 +1,131 @@ /*************************************************************************** * Copyright (C) 2008 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 "markerdialog.h" #include "core.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "mltcontroller/clipcontroller.h" #include "kdenlive_debug.h" #include #include #include #include "klocalizedstring.h" MarkerDialog::MarkerDialog(ClipController *clip, const CommentedTime &t, const Timecode &tc, const QString &caption, QWidget *parent) : QDialog(parent) , m_clip(clip) , m_dar(4.0 / 3.0) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setupUi(this); setWindowTitle(caption); // Set up categories for (int i = 0; i < 5; ++i) { marker_type->insertItem(i, i18n("Category %1", i)); marker_type->setItemData(i, CommentedTime::markerColor(i), Qt::DecorationRole); } marker_type->setCurrentIndex(t.markerType()); m_in = new TimecodeDisplay(tc, this); inputLayout->addWidget(m_in); m_in->setValue(t.time()); m_previewTimer = new QTimer(this); if (m_clip != nullptr) { m_in->setRange(0, m_clip->getFramePlaytime()); m_previewTimer->setInterval(500); connect(m_previewTimer, &QTimer::timeout, this, &MarkerDialog::slotUpdateThumb); m_dar = pCore->getCurrentDar(); int width = Kdenlive::DefaultThumbHeight * m_dar; QPixmap p(width, Kdenlive::DefaultThumbHeight); p.fill(Qt::transparent); switch (m_clip->clipType()) { - case Video: - case AV: - case SlideShow: - case Playlist: + case ClipType::Video: + case ClipType::AV: + case ClipType::SlideShow: + case ClipType::Playlist: m_previewTimer->start(); connect(this, SIGNAL(updateThumb()), m_previewTimer, SLOT(start())); break; - case Image: - case Text: - case QText: - case Color: + case ClipType::Image: + case ClipType::Text: + case ClipType::QText: + case ClipType::Color: m_previewTimer->start(); // p = m_clip->pixmap(m_in->getValue(), width, height); break; // UNKNOWN, AUDIO, VIRTUAL: default: p.fill(Qt::black); } if (!p.isNull()) { clip_thumb->setFixedWidth(p.width()); clip_thumb->setFixedHeight(p.height()); clip_thumb->setPixmap(p); } connect(m_in, &TimecodeDisplay::timeCodeEditingFinished, this, &MarkerDialog::updateThumb); } else { clip_thumb->setHidden(true); label_category->setHidden(true); marker_type->setHidden(true); } marker_comment->setText(t.comment()); marker_comment->selectAll(); marker_comment->setFocus(); adjustSize(); } MarkerDialog::~MarkerDialog() { delete m_previewTimer; } void MarkerDialog::slotUpdateThumb() { m_previewTimer->stop(); int pos = m_in->getValue(); int width = Kdenlive::DefaultThumbHeight * m_dar; /*m_image = KThumb::getFrame(m_producer, pos, swidth, width, 100); const QPixmap p = QPixmap::fromImage(m_image);*/ const QPixmap p = m_clip->pixmap(pos, width, Kdenlive::DefaultThumbHeight); if (!p.isNull()) { clip_thumb->setPixmap(p); } else { qCDebug(KDENLIVE_LOG) << "!!!!!!!!!!! ERROR CREATING THUMB"; } } QImage MarkerDialog::markerImage() const { return clip_thumb->pixmap()->toImage(); } CommentedTime MarkerDialog::newMarker() { KdenliveSettings::setDefault_marker_type(marker_type->currentIndex()); return CommentedTime(m_in->gentime(), marker_comment->text(), marker_type->currentIndex()); } diff --git a/src/doc/documentchecker.cpp b/src/doc/documentchecker.cpp index c5014b120..905fdd871 100644 --- a/src/doc/documentchecker.cpp +++ b/src/doc/documentchecker.cpp @@ -1,1132 +1,1132 @@ /*************************************************************************** * Copyright (C) 2008 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 "documentchecker.h" #include "kthumb.h" #include "kdenlivesettings.h" #include "titler/titlewidget.h" #include "utils/KoIconUtils.h" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include const int hashRole = Qt::UserRole; const int sizeRole = Qt::UserRole + 1; const int idRole = Qt::UserRole + 2; const int statusRole = Qt::UserRole + 3; const int typeRole = Qt::UserRole + 4; const int typeOriginalResource = Qt::UserRole + 5; const int clipTypeRole = Qt::UserRole + 6; const int CLIPMISSING = 0; const int CLIPOK = 1; const int CLIPPLACEHOLDER = 2; const int PROXYMISSING = 4; const int SOURCEMISSING = 5; const int LUMAMISSING = 10; const int LUMAOK = 11; const int LUMAPLACEHOLDER = 12; enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 }; DocumentChecker::DocumentChecker(const QUrl &url, const QDomDocument &doc) : m_url(url) , m_doc(doc) , m_dialog(nullptr) { } bool DocumentChecker::hasErrorInClips() { int max; QDomElement baseElement = m_doc.documentElement(); QString root = baseElement.attribute(QStringLiteral("root")); if (!root.isEmpty()) { QDir dir(root); if (!dir.exists()) { // Looks like project was moved, try recovering root from current project url m_rootReplacement.first = dir.absolutePath() + QDir::separator(); root = m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); baseElement.setAttribute(QStringLiteral("root"), root); root = QDir::cleanPath(root) + QDir::separator(); m_rootReplacement.second = root; } else { root = QDir::cleanPath(root) + QDir::separator(); } } // Check if strorage folder for temp files exists QString storageFolder; QDir projectDir(m_url.adjusted(QUrl::RemoveFilename).toLocalFile()); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == QStringLiteral("main bin")) { QString documentid = EffectsList::property(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid")); if (documentid.isEmpty()) { // invalid document id, recreate one documentid = QString::number(QDateTime::currentMSecsSinceEpoch()); // TODO: Warn on invalid doc id EffectsList::setProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid"), documentid); } storageFolder = EffectsList::property(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder")); if (!storageFolder.isEmpty() && QFileInfo(storageFolder).isRelative()) { storageFolder.prepend(root); } if (!storageFolder.isEmpty() && !QFile::exists(storageFolder) && projectDir.exists(documentid)) { storageFolder = projectDir.absolutePath(); EffectsList::setProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder"), projectDir.absoluteFilePath(documentid)); m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } break; } } QDomNodeList documentProducers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomElement profile = baseElement.firstChildElement(QStringLiteral("profile")); bool hdProfile = true; if (!profile.isNull()) { if (profile.attribute(QStringLiteral("width")).toInt() < 1000) { hdProfile = false; } } // List clips whose proxy is missing QList missingProxies; // List clips who have a working proxy but no source clip QList missingSources; m_safeImages.clear(); m_safeFonts.clear(); m_missingFonts.clear(); max = documentProducers.count(); QStringList verifiedPaths; QStringList serviceToCheck; serviceToCheck << QStringLiteral("kdenlivetitle") << QStringLiteral("qimage") << QStringLiteral("pixbuf") << QStringLiteral("timewarp") << QStringLiteral("framebuffer") << QStringLiteral("xml"); for (int i = 0; i < max; ++i) { QDomElement e = documentProducers.item(i).toElement(); QString service = EffectsList::property(e, QStringLiteral("mlt_service")); if (!service.startsWith(QLatin1String("avformat")) && !serviceToCheck.contains(service)) { continue; } if (service == QLatin1String("qtext")) { checkMissingImagesAndFonts(QStringList(), QStringList(EffectsList::property(e, QStringLiteral("family"))), e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } if (service == QLatin1String("kdenlivetitle")) { // TODO: Check is clip template is missing (xmltemplate) or hash changed QString xml = EffectsList::property(e, QStringLiteral("xmldata")); QStringList images = TitleWidget::extractImageList(xml); QStringList fonts = TitleWidget::extractFontList(xml); checkMissingImagesAndFonts(images, fonts, e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } QString resource = EffectsList::property(e, QStringLiteral("resource")); if (resource.isEmpty()) { continue; } if (service == QLatin1String("timewarp")) { // slowmotion clip, trim speed info resource = EffectsList::property(e, QStringLiteral("warp_resource")); } else if (service == QLatin1String("framebuffer")) { // slowmotion clip, trim speed info resource = resource.section(QLatin1Char('?'), 0, 0); } // Make sure to have absolute paths if (QFileInfo(resource).isRelative()) { resource.prepend(root); } if (verifiedPaths.contains(resource)) { // Don't check same url twice (for example track producers) continue; } QString proxy = EffectsList::property(e, QStringLiteral("kdenlive:proxy")); if (proxy.length() > 1) { if (QFileInfo(proxy).isRelative()) { proxy.prepend(root); } if (!QFile::exists(proxy)) { // Missing clip found // Check if proxy exists in current storage folder bool fixed = false; if (!storageFolder.isEmpty()) { QDir dir(storageFolder + QStringLiteral("/proxy/")); if (dir.exists(QFileInfo(proxy).fileName())) { QString updatedPath = dir.absoluteFilePath(QFileInfo(proxy).fileName()); fixProxyClip(e.attribute(QStringLiteral("id")), EffectsList::property(e, QStringLiteral("kdenlive:proxy")), updatedPath, documentProducers); fixed = true; } } if (!fixed) { missingProxies.append(e); } } QString original = EffectsList::property(e, QStringLiteral("kdenlive:originalurl")); if (QFileInfo(original).isRelative()) { original.prepend(root); } // Check for slideshows bool slideshow = original.contains(QStringLiteral("/.all.")) || original.contains(QLatin1Char('?')) || original.contains(QLatin1Char('%')); if (slideshow && !EffectsList::property(e, QStringLiteral("ttl")).isEmpty()) { original = QFileInfo(original).absolutePath(); } if (!QFile::exists(original)) { // clip has proxy but original clip is missing missingSources.append(e); } verifiedPaths.append(resource); continue; } // Check for slideshows bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if ((service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) && slideshow) { resource = QFileInfo(resource).absolutePath(); } if (!QFile::exists(resource)) { // Missing clip found m_missingClips.append(e); } // Make sure we don't query same path twice verifiedPaths.append(resource); } // Get list of used Luma files QStringList missingLumas; QStringList filesToCheck; QString filePath; QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); max = trans.count(); for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (service == QLatin1String("luma")) { luma = getProperty(transition, QStringLiteral("resource")); } else if (service == QLatin1String("composite")) { luma = getProperty(transition, QStringLiteral("luma")); } if (!luma.isEmpty() && !filesToCheck.contains(luma)) { filesToCheck.append(luma); } } QMap autoFixLuma; // Check existence of luma files for (const QString &lumafile : filesToCheck) { filePath = lumafile; if (QFileInfo(filePath).isRelative()) { filePath.prepend(root); } if (!QFile::exists(filePath)) { QString fixedLuma; // check if this was an old format luma, not in correct folder fixedLuma = filePath.section(QLatin1Char('/'), 0, -2); fixedLuma.append(hdProfile ? QStringLiteral("/HD/") : QStringLiteral("/PAL/")); fixedLuma.append(filePath.section(QLatin1Char('/'), -1)); if (QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); continue; } if (filePath.endsWith(QLatin1String(".pgm"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".png"); } else if (filePath.endsWith(QLatin1String(".png"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".pgm"); } if (!fixedLuma.isEmpty() && QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); } else { missingLumas.append(lumafile); } } } if (!autoFixLuma.isEmpty()) { for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (service == QLatin1String("luma")) { luma = getProperty(transition, QStringLiteral("resource")); } else if (service == QLatin1String("composite")) { luma = getProperty(transition, QStringLiteral("luma")); } if (!luma.isEmpty() && autoFixLuma.contains(luma)) { setProperty(transition, service == QLatin1String("luma") ? QStringLiteral("resource") : QStringLiteral("luma"), autoFixLuma.value(luma)); } } } if (m_missingClips.isEmpty() && missingLumas.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty() && m_missingFonts.isEmpty()) { return false; } m_dialog = new QDialog(); m_dialog->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_ui.setupUi(m_dialog); for (const QString &l : missingLumas) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l); item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-close"))); item->setData(0, idRole, l); item->setData(0, statusRole, LUMAMISSING); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_missingClips.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty()); max = m_missingClips.count(); m_missingProxyIds.clear(); for (int i = 0; i < max; ++i) { QDomElement e = m_missingClips.at(i).toElement(); QString clipType; ClipType type; int status = CLIPMISSING; const QString service = EffectsList::property(e, QStringLiteral("mlt_service")); QString resource = service == QLatin1String("timewarp") ? EffectsList::property(e, QStringLiteral("warp_resource")) : EffectsList::property(e, QStringLiteral("resource")); bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if (service == QLatin1String("avformat") || service == QLatin1String("avformat-novalidate") || service == QLatin1String("framebuffer") || service == QLatin1String("timewarp")) { clipType = i18n("Video clip"); - type = AV; + type = ClipType::AV; } else if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { if (slideshow) { clipType = i18n("Slideshow clip"); - type = SlideShow; + type = ClipType::SlideShow; } else { clipType = i18n("Image clip"); - type = Image; + type = ClipType::Image; } } else if (service == QLatin1String("mlt") || service == QLatin1String("xml")) { clipType = i18n("Playlist clip"); - type = Playlist; + type = ClipType::Playlist; } else if (e.tagName() == QLatin1String("missingtitle")) { clipType = i18n("Title Image"); status = TITLE_IMAGE_ELEMENT; - type = Text; + type = ClipType::Text; } else { clipType = i18n("Unknown"); - type = Unknown; + type = ClipType::Unknown; } QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPMISSING); - item->setData(0, clipTypeRole, type); + item->setData(0, clipTypeRole, (int)type); item->setData(0, idRole, e.attribute(QStringLiteral("id"))); item->setToolTip(0, i18n("Missing item")); if (status == TITLE_IMAGE_ELEMENT) { item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-warning"))); item->setToolTip(1, e.attribute(QStringLiteral("name"))); QString imageResource = e.attribute(QStringLiteral("resource")); item->setData(0, typeRole, status); item->setData(0, typeOriginalResource, e.attribute(QStringLiteral("resource"))); if (!m_rootReplacement.first.isEmpty()) { if (imageResource.startsWith(m_rootReplacement.first)) { imageResource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(imageResource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, imageResource); } else { item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-close"))); if (QFileInfo(resource).isRelative()) { resource.prepend(root); } item->setData(0, hashRole, EffectsList::property(e, QStringLiteral("kdenlive:file_hash"))); item->setData(0, sizeRole, EffectsList::property(e, QStringLiteral("kdenlive:file_size"))); if (!m_rootReplacement.first.isEmpty()) { if (resource.startsWith(m_rootReplacement.first)) { resource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(resource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, resource); } } for (const QString &font : m_missingFonts) { QString clipType = i18n("Title Font"); QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPPLACEHOLDER); item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-warning"))); QString newft = QFontInfo(QFont(font)).family(); item->setText(1, i18n("%1 will be replaced by %2", font, newft)); item->setData(0, typeRole, CLIPMISSING); } if (!m_missingClips.isEmpty()) { m_ui.infoLabel->setText(i18n("The project file contains missing clips or files")); } if (!missingProxies.isEmpty()) { if (!m_ui.infoLabel->text().isEmpty()) { m_ui.infoLabel->setText(m_ui.infoLabel->text() + QStringLiteral(". ")); } m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18n("Missing proxies will be recreated after opening.")); } if (!missingSources.isEmpty()) { if (!m_ui.infoLabel->text().isEmpty()) { m_ui.infoLabel->setText(m_ui.infoLabel->text() + QStringLiteral(". ")); } m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18np("The project file contains a missing clip, you can still work with its proxy.", "The project file contains %1 missing clips, you can still work with their proxies.", missingSources.count())); } m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty()); m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty() || !missingSources.isEmpty()); m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty()); // Check missing proxies max = missingProxies.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip")); item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-warning"))); item->setText( 1, i18np("%1 missing proxy clip, will be recreated on project opening", "%1 missing proxy clips, will be recreated on project opening", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, PROXYMISSING); item->setToolTip(0, i18n("Missing proxy")); } for (int i = 0; i < max; ++i) { QDomElement e = missingProxies.at(i).toElement(); QString realPath = EffectsList::property(e, QStringLiteral("kdenlive:originalurl")); QString id = e.attribute(QStringLiteral("id")); m_missingProxyIds << id; // Tell Kdenlive to recreate proxy e.setAttribute(QStringLiteral("_replaceproxy"), QStringLiteral("1")); // Replace proxy url with real clip in MLT producers QDomElement mltProd; int prodsCount = documentProducers.count(); for (int j = 0; j < prodsCount; ++j) { mltProd = documentProducers.at(j).toElement(); QString prodId = mltProd.attribute(QStringLiteral("id")); QString parentId = prodId; bool slowmotion = false; if (parentId.startsWith(QLatin1String("slowmotion"))) { slowmotion = true; parentId = parentId.section(QLatin1Char(':'), 1, 1); } if (parentId.contains(QLatin1Char('_'))) { parentId = parentId.section(QLatin1Char('_'), 0, 0); } if (parentId == id) { // Hit, we must replace url QString suffix; QString resource = EffectsList::property(mltProd, QStringLiteral("resource")); if (slowmotion) { suffix = QLatin1Char('?') + resource.section(QLatin1Char('?'), -1); } EffectsList::setProperty(mltProd, QStringLiteral("resource"), realPath + suffix); if (prodId == id) { // Only set proxy property on master producer EffectsList::setProperty(mltProd, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } } } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } // Check clips with available proxies but missing original source clips max = missingSources.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip")); item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-warning"))); item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, SOURCEMISSING); item->setToolTip(0, i18n("Missing source clip")); for (int i = 0; i < max; ++i) { QDomElement e = missingSources.at(i).toElement(); QString realPath = EffectsList::property(e, QStringLiteral("kdenlive:originalurl")); QString id = e.attribute(QStringLiteral("id")); // Tell Kdenlive the source is missing e.setAttribute(QStringLiteral("_missingsource"), QStringLiteral("1")); QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip")); // qCDebug(KDENLIVE_LOG)<<"// Adding missing source clip: "<setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-close"))); subitem->setText(1, realPath); subitem->setData(0, hashRole, EffectsList::property(e, QStringLiteral("kdenlive:file_hash"))); subitem->setData(0, sizeRole, EffectsList::property(e, QStringLiteral("kdenlive:file_size"))); subitem->setData(0, statusRole, CLIPMISSING); // int t = e.attribute("type").toInt(); subitem->setData(0, typeRole, EffectsList::property(e, QStringLiteral("mlt_service"))); subitem->setData(0, idRole, id); } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } m_ui.treeWidget->resizeColumnToContents(0); connect(m_ui.recursiveSearch, &QAbstractButton::pressed, this, &DocumentChecker::slotSearchClips); connect(m_ui.usePlaceholders, &QAbstractButton::pressed, this, &DocumentChecker::slotPlaceholders); connect(m_ui.removeSelected, &QAbstractButton::pressed, this, &DocumentChecker::slotDeleteSelected); connect(m_ui.treeWidget, &QTreeWidget::itemDoubleClicked, this, &DocumentChecker::slotEditItem); connect(m_ui.treeWidget, &QTreeWidget::itemSelectionChanged, this, &DocumentChecker::slotCheckButtons); // adjustSize(); if (m_ui.treeWidget->topLevelItem(0)) { m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0)); } checkStatus(); int acceptMissing = m_dialog->exec(); if (acceptMissing == QDialog::Accepted) { acceptDialog(); } return (acceptMissing != QDialog::Accepted); } DocumentChecker::~DocumentChecker() { delete m_dialog; } QString DocumentChecker::getProperty(const QDomElement &effect, const QString &name) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { return e.firstChild().nodeValue(); } } return QString(); } void DocumentChecker::setProperty(const QDomElement &effect, const QString &name, const QString &value) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { e.firstChild().setNodeValue(value); } } } void DocumentChecker::slotSearchClips() { // QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QString clipFolder = m_url.adjusted(QUrl::RemoveFilename).toLocalFile(); QString newpath = QFileDialog::getExistingDirectory(qApp->activeWindow(), i18n("Clips folder"), clipFolder); if (newpath.isEmpty()) { return; } int ix = 0; bool fixed = false; m_ui.recursiveSearch->setChecked(true); // TODO: make non modal QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); QDir searchDir(newpath); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { QTreeWidgetItem *subchild = child->child(j); QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString(), subchild->text(1)); if (!clipPath.isEmpty()) { fixed = true; subchild->setText(1, clipPath); subchild->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); subchild->setData(0, statusRole, CLIPOK); } } } else if (child->data(0, statusRole).toInt() == CLIPMISSING) { bool perfectMatch = true; ClipType type = (ClipType)child->data(0, clipTypeRole).toInt(); QString clipPath; - if (type != SlideShow) { + if (type != ClipType::SlideShow) { // Slideshows cannot be found with hash / size clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString(), child->text(1)); } if (clipPath.isEmpty()) { clipPath = searchPathRecursively(searchDir, QUrl::fromLocalFile(child->text(1)).fileName(), type); perfectMatch = false; } if (!clipPath.isEmpty()) { fixed = true; child->setText(1, clipPath); child->setIcon(0, perfectMatch ? KoIconUtils::themedIcon(QStringLiteral("dialog-ok")) : KoIconUtils::themedIcon(QStringLiteral("dialog-warning"))); child->setData(0, statusRole, CLIPOK); } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { QString fileName = searchLuma(searchDir, child->data(0, idRole).toString()); if (!fileName.isEmpty()) { fixed = true; child->setText(1, fileName); child->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, LUMAOK); } } else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) { // Search missing title images QString missingFileName = QUrl::fromLocalFile(child->text(1)).fileName(); QString newPath = searchPathRecursively(searchDir, missingFileName); if (!newPath.isEmpty()) { // File found fixed = true; child->setText(1, newPath); child->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, CLIPOK); } } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.recursiveSearch->setChecked(false); m_ui.recursiveSearch->setEnabled(true); if (fixed) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } checkStatus(); } QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const { QDir searchPath(KdenliveSettings::mltpath()); QString fname = QUrl::fromLocalFile(file).fileName(); if (file.contains(QStringLiteral("PAL"))) { searchPath.cd(QStringLiteral("../lumas/PAL")); } else { searchPath.cd(QStringLiteral("../lumas/NTSC")); } QFileInfo result(searchPath, fname); if (result.exists()) { return result.filePath(); } // try to find luma in application path searchPath.setPath(QCoreApplication::applicationDirPath()); #ifdef Q_OS_WIN searchPath.cd(QStringLiteral("data/lumas")); #else searchPath.cd(QStringLiteral("../share/apps/kdenlive/lumas")); #endif result.setFile(searchPath, fname); if (result.exists()) { return result.filePath(); } // Try in Kdenlive's standard KDE path QString res = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("lumas/") + fname); if (!res.isEmpty()) { return res; } // Try in user's chosen folder return searchPathRecursively(dir, fname); } QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName, ClipType type) const { QString foundFileName; QStringList filters; - if (type == SlideShow) { + if (type == ClipType::SlideShow) { if (fileName.contains(QLatin1Char('%'))) { filters << fileName.section(QLatin1Char('%'), 0, -2) + QLatin1Char('*'); } else { return QString(); } } else { filters << fileName; } QDir searchDir(dir); searchDir.setNameFilters(filters); QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable); if (!filesAndDirs.isEmpty()) { // File Found - if (type == SlideShow) { + if (type == ClipType::SlideShow) { return searchDir.absoluteFilePath(fileName); } return searchDir.absoluteFilePath(filesAndDirs.at(0)); } searchDir.setNameFilters(QStringList()); filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName, type); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) const { if (matchSize.isEmpty() && matchHash.isEmpty()) { return searchPathRecursively(dir, QUrl::fromLocalFile(fileName).fileName()); } QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (QString::number(file.size()) == matchSize) { if (file.open(QIODevice::ReadOnly)) { /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString::fromLatin1(fileHash.toHex()) == matchHash) { return file.fileName(); } } } ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash, fileName); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int) { int t = item->data(0, typeRole).toInt(); if (t == TITLE_FONT_ELEMENT) { return; } //|| t == TITLE_IMAGE_ELEMENT) { QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(item->text(1)), m_dialog, i18n("Enter new location for file")); if (!url.isValid()) { return; } item->setText(1, url.toLocalFile()); ClipType type = (ClipType)item->data(0, clipTypeRole).toInt(); bool fixed = false; - if (type == SlideShow && QFile::exists(url.adjusted(QUrl::RemoveFilename).toLocalFile())) { + if (type == ClipType::SlideShow && QFile::exists(url.adjusted(QUrl::RemoveFilename).toLocalFile())) { fixed = true; } if (fixed || QFile::exists(url.toLocalFile())) { item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPOK); } else { item->setData(0, statusRole, LUMAOK); } checkStatus(); } else { item->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-close"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPMISSING); } else { item->setData(0, statusRole, LUMAMISSING); } checkStatus(); } } void DocumentChecker::acceptDialog() { QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int ix = 0; // prepare transitions QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); // Mark document as modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { fixSourceClipItem(child->child(j), producers); } } else { fixClipItem(child, producers, trans); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } // QDialog::accept(); } void DocumentChecker::fixProxyClip(const QString &id, const QString &oldUrl, const QString &newUrl, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString sourceId = e.attribute(QStringLiteral("id")); QString parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } if (parentId == id) { // Fix clip QString resource = EffectsList::property(e, QStringLiteral("resource")); // TODO: Slowmmotion clips if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { // fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (resource == oldUrl) { EffectsList::setProperty(e, QStringLiteral("resource"), newUrl); } if (sourceId == id) { // Only set originalurl on master producer EffectsList::setProperty(e, QStringLiteral("kdenlive:proxy"), newUrl); } } } } void DocumentChecker::fixSourceClipItem(QTreeWidgetItem *child, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; // int t = child->data(0, typeRole).toInt(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString id = child->data(0, idRole).toString(); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString sourceId = e.attribute(QStringLiteral("id")); QString parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } if (parentId == id) { // Fix clip QString resource = EffectsList::property(e, QStringLiteral("resource")); QString fixedResource = child->text(1); if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (sourceId == id) { // Only set originalurl on master producer EffectsList::setProperty(e, QStringLiteral("kdenlive:originalurl"), fixedResource); } if (m_missingProxyIds.contains(parentId)) { // Proxy is also missing, replace resource EffectsList::setProperty(e, QStringLiteral("resource"), fixedResource); } } } } } void DocumentChecker::fixClipItem(QTreeWidgetItem *child, const QDomNodeList &producers, const QDomNodeList &trans) { QDomElement e, property; QDomNodeList properties; int t = child->data(0, typeRole).toInt(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString id = child->data(0, idRole).toString(); QString fixedResource = child->text(1); if (t == TITLE_IMAGE_ELEMENT) { // edit images embedded in titles for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0) == id) { // Fix clip properties = e.childNodes(); for (int j = 0; j < properties.count(); ++j) { property = properties.item(j).toElement(); if (property.attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { QString xml = property.firstChild().nodeValue(); xml.replace(child->data(0, typeOriginalResource).toString(), fixedResource); property.firstChild().setNodeValue(xml); break; } } } } } else { // edit clip url /*for (int i = 0; i < infoproducers.count(); ++i) { e = infoproducers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip e.setAttribute("resource", child->text(1)); e.setAttribute("name", QUrl(child->text(1)).fileName()); e.removeAttribute("_missingsource"); break; } }*/ for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0) == id || e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1) == id || e.attribute(QStringLiteral("id")) == id) { // Fix clip QString resource = getProperty(e, QStringLiteral("resource")); QString service = getProperty(e, QStringLiteral("mlt_service")); QString updatedResource = fixedResource; if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { updatedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (service == QLatin1String("timewarp")) { setProperty(e, QStringLiteral("warp_resource"), updatedResource); updatedResource.prepend(getProperty(e, QStringLiteral("warp_speed")) + QLatin1Char(':')); } setProperty(e, QStringLiteral("resource"), updatedResource); } } } } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) { // QString id = child->data(0, idRole).toString(); /*for (int i = 0; i < infoproducers.count(); ++i) { e = infoproducers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip e.setAttribute("placeholder", '1'); break; } }*/ } else if (child->data(0, statusRole).toInt() == LUMAOK) { for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (service == QLatin1String("luma")) { luma = getProperty(trans.at(i).toElement(), QStringLiteral("resource")); } else if (service == QLatin1String("composite")) { luma = getProperty(trans.at(i).toElement(), QStringLiteral("luma")); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { setProperty(trans.at(i).toElement(), service == QLatin1String("luma") ? QStringLiteral("resource") : QStringLiteral("luma"), child->text(1)); // qCDebug(KDENLIVE_LOG) << "replace with; " << child->text(1); } } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (service == QLatin1String("luma")) { luma = getProperty(trans.at(i).toElement(), QStringLiteral("resource")); } else if (service == QLatin1String("composite")) { luma = getProperty(trans.at(i).toElement(), QStringLiteral("luma")); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { setProperty(trans.at(i).toElement(), service == QLatin1String("luma") ? QStringLiteral("resource") : QStringLiteral("luma"), QString()); } } } } void DocumentChecker::slotPlaceholders() { int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == CLIPMISSING) { child->setData(0, statusRole, CLIPPLACEHOLDER); child->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { child->setData(0, statusRole, LUMAPLACEHOLDER); child->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("dialog-ok"))); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } checkStatus(); } void DocumentChecker::checkStatus() { bool status = true; int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { int childStatus = child->data(0, statusRole).toInt(); if (childStatus == CLIPMISSING) { status = false; break; } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status); } void DocumentChecker::slotDeleteSelected() { if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) { return; } QStringList deletedIds; QStringList deletedLumas; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (QTreeWidgetItem *child : m_ui.treeWidget->selectedItems()) { int id = child->data(0, statusRole).toInt(); if (id == CLIPMISSING) { deletedIds.append(child->data(0, idRole).toString()); delete child; } else if (id == LUMAMISSING) { deletedLumas.append(child->data(0, idRole).toString()); delete child; } } if (!deletedLumas.isEmpty()) { QDomElement e; QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); for (const QString &lumaPath : deletedLumas) { for (int i = 0; i < transitions.count(); ++i) { e = transitions.item(i).toElement(); QString service = EffectsList::property(e, QStringLiteral("mlt_service")); QString resource; if (service == QLatin1String("luma")) { resource = EffectsList::property(e, QStringLiteral("resource")); } else if (service == QLatin1String("composite")) { resource = EffectsList::property(e, QStringLiteral("luma")); } if (resource == lumaPath) { EffectsList::removeProperty(e, service == QLatin1String("luma") ? QStringLiteral("resource") : QStringLiteral("luma")); } } } } if (!deletedIds.isEmpty()) { QDomElement e; QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); // QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer"); QDomNode mlt = m_doc.elementsByTagName(QStringLiteral("mlt")).at(0); QDomNode kdenlivedoc = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); /*for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); ++i) { e = infoproducers.item(i).toElement(); if (deletedIds.contains(e.attribute("id"))) { // Remove clip kdenlivedoc.removeChild(e); --i; j++; } }*/ for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Remove clip mlt.removeChild(e); --i; } } for (int i = 0; i < playlists.count(); ++i) { QDomNodeList entries = playlists.at(i).toElement().elementsByTagName(QStringLiteral("entry")); for (int j = 0; j < entries.count(); ++j) { e = entries.item(j).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Replace clip with blank while (e.childNodes().count() > 0) { e.removeChild(e.firstChild()); } e.setTagName(QStringLiteral("blank")); e.removeAttribute(QStringLiteral("producer")); int length = e.attribute(QStringLiteral("out")).toInt() - e.attribute(QStringLiteral("in")).toInt(); e.setAttribute(QStringLiteral("length"), length); j--; } } } m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); checkStatus(); } } void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip) { QDomDocument doc; for (const QString &img : images) { if (m_safeImages.contains(img)) { continue; } if (!QFile::exists(img)) { QDomElement e = doc.createElement(QStringLiteral("missingtitle")); e.setAttribute(QStringLiteral("type"), TITLE_IMAGE_ELEMENT); e.setAttribute(QStringLiteral("resource"), img); e.setAttribute(QStringLiteral("id"), id); e.setAttribute(QStringLiteral("name"), baseClip); m_missingClips.append(e); } else { m_safeImages.append(img); } } for (const QString &fontelement : fonts) { if (m_safeFonts.contains(fontelement)) { continue; } QFont f(fontelement); ////qCDebug(KDENLIVE_LOG) << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family(); if (fontelement != QFontInfo(f).family()) { m_missingFonts << fontelement; } else { m_safeFonts.append(fontelement); } } } void DocumentChecker::slotCheckButtons() { if (m_ui.treeWidget->currentItem()) { QTreeWidgetItem *item = m_ui.treeWidget->currentItem(); int t = item->data(0, typeRole).toInt(); int s = item->data(0, statusRole).toInt(); if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) { m_ui.removeSelected->setEnabled(false); } else { m_ui.removeSelected->setEnabled(true); } } } diff --git a/src/doc/documentchecker.h b/src/doc/documentchecker.h index b973c10ee..722649e7d 100644 --- a/src/doc/documentchecker.h +++ b/src/doc/documentchecker.h @@ -1,81 +1,81 @@ /*************************************************************************** * Copyright (C) 2008 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 DOCUMENTCHECKER_H #define DOCUMENTCHECKER_H #include "definitions.h" #include "ui_missingclips_ui.h" #include #include #include class DocumentChecker : public QObject { Q_OBJECT public: explicit DocumentChecker(const QUrl &url, const QDomDocument &doc); ~DocumentChecker(); /** * @brief checks for problems with the clips in the project * Checks for missing proxies, wrong duration clips, missing fonts, missing images, missing source clips * Calls DocumentChecker::checkMissingImagesAndFonts () /n * Called by KdenliveDoc::checkDocumentClips () /n * @return */ bool hasErrorInClips(); private slots: void acceptDialog(); void slotSearchClips(); void slotEditItem(QTreeWidgetItem *item, int); void slotPlaceholders(); void slotDeleteSelected(); QString getProperty(const QDomElement &effect, const QString &name); void setProperty(const QDomElement &effect, const QString &name, const QString &value); QString searchLuma(const QDir &dir, const QString &file) const; /** @brief Check if images and fonts in this clip exists, returns a list of images that do exist so we don't check twice. */ void checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip); void slotCheckButtons(); private: QUrl m_url; QDomDocument m_doc; Ui::MissingClips_UI m_ui; QDialog *m_dialog; QPair m_rootReplacement; - QString searchPathRecursively(const QDir &dir, const QString &fileName, ClipType type = Unknown) const; + QString searchPathRecursively(const QDir &dir, const QString &fileName, ClipType type = ClipType::Unknown) const; QString searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) const; void checkStatus(); QMap m_missingTitleImages; QMap m_missingTitleFonts; QList m_missingClips; QStringList m_missingFonts; QStringList m_safeImages; QStringList m_safeFonts; QStringList m_missingProxyIds; void fixClipItem(QTreeWidgetItem *child, const QDomNodeList &producers, const QDomNodeList &trans); void fixSourceClipItem(QTreeWidgetItem *child, const QDomNodeList &producers); void fixProxyClip(const QString &id, const QString &oldUrl, const QString &newUrl, const QDomNodeList &producers); }; #endif diff --git a/src/doc/documentvalidator.cpp b/src/doc/documentvalidator.cpp index 711aa1bf2..b53fe8b5f 100644 --- a/src/doc/documentvalidator.cpp +++ b/src/doc/documentvalidator.cpp @@ -1,2175 +1,2175 @@ /*************************************************************************** * Copyright (C) 2007 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 "documentvalidator.h" #include "core.h" #include "definitions.h" #include "effectslist/initeffects.h" #include "mainwindow.h" #include "mltcontroller/bincontroller.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif #include DocumentValidator::DocumentValidator(const QDomDocument &doc, const QUrl &documentUrl) : m_doc(doc) , m_url(documentUrl) , m_modified(false) { } bool DocumentValidator::validate(const double currentVersion) { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); // At least the root element must be there if (mlt.isNull()) { return false; } QDomElement kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); QString rootDir = mlt.attribute(QStringLiteral("root")); if (rootDir == QLatin1String("$CURRENTPATH")) { // The document was extracted from a Kdenlive archived project, fix root directory QString playlist = m_doc.toString(); playlist.replace(QLatin1String("$CURRENTPATH"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); m_doc.setContent(playlist); mlt = m_doc.firstChildElement(QStringLiteral("mlt")); kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); } else if (rootDir.isEmpty()) { mlt.setAttribute(QStringLiteral("root"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); } // Previous MLT / Kdenlive versions used C locale by default QLocale documentLocale = QLocale::c(); if (mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { // Check document numeric separator (added in Kdenlive 16.12.1 QDomElement main_playlist = mlt.firstChildElement(QStringLiteral("playlist")); QDomNodeList props = main_playlist.elementsByTagName(QStringLiteral("property")); QChar numericalSeparator; for (int i = 0; i < props.count(); ++i) { QDomNode n = props.at(i); if (n.toElement().attribute(QStringLiteral("name")) == QLatin1String("kdenlive:docproperties.decimalPoint")) { QString sep = n.firstChild().nodeValue(); if (!sep.isEmpty()) { numericalSeparator = sep.at(0); } break; } } bool error = false; if (!numericalSeparator.isNull() && numericalSeparator != QLocale().decimalPoint()) { qCDebug(KDENLIVE_LOG) << " * ** LOCALE CHANGE REQUIRED: " << numericalSeparator << "!=" << QLocale().decimalPoint() << " / " << QLocale::system().decimalPoint(); // Change locale to match document QString requestedLocale = mlt.attribute(QStringLiteral("LC_NUMERIC")); documentLocale = QLocale(requestedLocale); #ifdef Q_OS_WIN // Most locales don't work on windows, so use C whenever possible if (numericalSeparator == QLatin1Char('.')) { #else if (numericalSeparator != documentLocale.decimalPoint() && numericalSeparator == QLatin1Char('.')) { #endif requestedLocale = QStringLiteral("C"); documentLocale = QLocale::c(); } #ifdef Q_OS_MAC setlocale(LC_NUMERIC_MASK, requestedLocale.toUtf8().constData()); #elif defined(Q_OS_WIN) std::locale::global(std::locale(requestedLocale.toUtf8().constData())); #else setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif if (numericalSeparator != documentLocale.decimalPoint()) { // Parse installed locales to find one matching const QList list = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale().script(), QLocale::AnyCountry); QLocale matching; for (const QLocale &loc : list) { if (loc.decimalPoint() == numericalSeparator) { matching = loc; qCDebug(KDENLIVE_LOG) << "Warning, document locale: " << mlt.attribute(QStringLiteral("LC_NUMERIC")) << " is not available, using: " << loc.name(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, loc.name().toUtf8().constData()); #else setlocale(LC_NUMERIC_MASK, loc.name().toUtf8().constData()); #endif documentLocale = matching; break; } } error = numericalSeparator != documentLocale.decimalPoint(); } } else if (numericalSeparator.isNull()) { // Change locale to match document #ifndef Q_OS_MAC const QString newloc = QString::fromLatin1(setlocale(LC_NUMERIC, mlt.attribute(QStringLiteral("LC_NUMERIC")).toUtf8().constData())); #else const QString newloc = setlocale(LC_NUMERIC_MASK, mlt.attribute("LC_NUMERIC").toUtf8().constData()); #endif documentLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); error = newloc.isEmpty(); } else { // Document separator matching system separator documentLocale = QLocale(); } if (error) { // Requested locale not available, ask for install KMessageBox::sorry(QApplication::activeWindow(), i18n("The document was created in \"%1\" locale, which is not installed on your system. Please install that language pack. " "Until then, Kdenlive might not be able to correctly open the document.", mlt.attribute(QStringLiteral("LC_NUMERIC")))); } // Make sure Qt locale and C++ locale have the same numeric separator, might not be the case // With some locales since C++ and Qt use a different database for locales // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QString(documentLocale.decimalPoint())) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict on your system. The document uses locale %1 which uses a \"%2\" as numeric separator (in " "system libraries) but Qt expects \"%3\". You might not be able to correctly open the project.", mlt.attribute(QStringLiteral("LC_NUMERIC")), documentLocale.decimalPoint(), separator)); // qDebug()<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to at least have correct decimal point if (strncmp(separator, ".", 1) == 0) { documentLocale = QLocale::c(); } else if (strncmp(separator, ",", 1) == 0) { documentLocale = QLocale(QStringLiteral("fr_FR.UTF-8")); } } #endif } documentLocale.setNumberOptions(QLocale::OmitGroupSeparator); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default if (!mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif } QLocale::setDefault(documentLocale); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict. The document uses a \"%1\" as numeric separator, but your computer is configured to use " "\"%2\". Change your computer settings or you might not be able to correctly open the project.", documentLocale.decimalPoint(), QLocale().decimalPoint())); } // locale conversion might need to be redone #ifndef Q_OS_MAC initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); #else initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC_MASK, nullptr))); #endif } double version = -1; if (kdenliveDoc.isNull() || !kdenliveDoc.hasAttribute(QStringLiteral("version"))) { // Newer Kdenlive document version QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); version = EffectsList::property(main, QStringLiteral("kdenlive:docproperties.version")).toDouble(); } else { bool ok; version = documentLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { // Could not parse version number, there is probably a conflict in decimal separator QLocale tempLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); version = tempLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { version = kdenliveDoc.attribute(QStringLiteral("version")).toDouble(&ok); } if (!ok) { // Last try: replace comma with a dot QString versionString = kdenliveDoc.attribute(QStringLiteral("version")); if (versionString.contains(QLatin1Char(','))) { versionString.replace(QLatin1Char(','), QLatin1Char('.')); } version = versionString.toDouble(&ok); if (!ok) { qCDebug(KDENLIVE_LOG) << "// CANNOT PARSE VERSION NUMBER, ERROR!"; } } } } // Upgrade the document to the latest version if (!upgrade(version, currentVersion)) { return false; } checkOrphanedProducers(); return true; /* // Check the syntax (this will be replaced by XSD validation with Qt 4.6) // and correct some errors { // Return (or create) the tractor QDomElement tractor = mlt.firstChildElement("tractor"); if (tractor.isNull()) { m_modified = true; tractor = m_doc.createElement("tractor"); tractor.setAttribute("global_feed", "1"); tractor.setAttribute("in", "0"); tractor.setAttribute("out", "-1"); tractor.setAttribute("id", "maintractor"); mlt.appendChild(tractor); } // Make sure at least one track exists, and they're equal in number to // to the maximum between MLT and Kdenlive playlists and tracks // // In older Kdenlive project files, one playlist is not a real track (the black track), we have: track count = playlist count- 1 // In newer Qt5 Kdenlive, the Bin playlist should not appear as a track. So we should have: track count = playlist count- 2 int trackOffset = 1; QDomNodeList playlists = m_doc.elementsByTagName("playlist"); // Remove "main bin" playlist that simply holds the bin's clips and is not a real playlist for (int i = 0; i < playlists.count(); ++i) { QString playlistId = playlists.at(i).toElement().attribute("id"); if (playlistId == BinController::binPlaylistId()) { // remove pseudo-playlist //playlists.at(i).parentNode().removeChild(playlists.at(i)); trackOffset = 2; break; } } int tracksMax = playlists.count() - trackOffset; // Remove the black track and bin track QDomNodeList tracks = tractor.elementsByTagName("track"); tracksMax = qMax(tracks.count() - 1, tracksMax); QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo"); tracksMax = qMax(tracksinfo.count(), tracksMax); tracksMax = qMax(1, tracksMax); // Force existence of one track if (playlists.count() - trackOffset < tracksMax || tracks.count() < tracksMax || tracksinfo.count() < tracksMax) { qCDebug(KDENLIVE_LOG) << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK"; m_modified = true; int difference; // use the MLT tracks as reference if (tracks.count() - 1 < tracksMax) { // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one. if (tracksinfo.count() != tracks.count() - 1) { // The Kdenlive tracks are not ok, clear and rebuild them QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo"); QDomNode tnode = tinfo.firstChild(); while (!tnode.isNull()) { tinfo.removeChild(tnode); tnode = tinfo.firstChild(); } for (int i = 1; i < tracks.count(); ++i) { QString hide = tracks.at(i).toElement().attribute("hide"); QDomElement newTrack = m_doc.createElement("trackinfo"); if (hide == "video") { // audio track; newTrack.setAttribute("type", "audio"); newTrack.setAttribute("blind", 1); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } else { newTrack.setAttribute("blind", 0); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } tinfo.appendChild(newTrack); } } } if (playlists.count() - 1 < tracksMax) { difference = tracksMax - (playlists.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement playlist = m_doc.createElement("playlist"); mlt.appendChild(playlist); } } if (tracks.count() - 1 < tracksMax) { difference = tracksMax - (tracks.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement track = m_doc.createElement("track"); tractor.appendChild(track); } } if (tracksinfo.count() < tracksMax) { QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo"); if (tracksinfoElm.isNull()) { tracksinfoElm = m_doc.createElement("tracksinfo"); kdenliveDoc.appendChild(tracksinfoElm); } difference = tracksMax - tracksinfo.count(); for (int i = 0; i < difference; ++i) { QDomElement trackinfo = m_doc.createElement("trackinfo"); trackinfo.setAttribute("mute", "0"); trackinfo.setAttribute("locked", "0"); tracksinfoElm.appendChild(trackinfo); } } } // TODO: check the tracks references // TODO: check internal mix transitions } updateEffects(); return true; */ } bool DocumentValidator::upgrade(double version, const double currentVersion) { qCDebug(KDENLIVE_LOG) << "Opening a document with version " << version << " / " << currentVersion; // No conversion needed if (qAbs(version - currentVersion) < 0.001) { return true; } // The document is too new if (version > currentVersion) { // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry( QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.\nPlease consider upgrading your Kdenlive version.", version), i18n("Unable to open project")); return false; } // Unsupported document versions if (version == 0.5 || version == 0.7) { // 0.7 is unsupported // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry(QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project")); return false; } // QDomNode infoXmlNode; QDomElement infoXml; QDomNodeList docs = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")); if (!docs.isEmpty()) { infoXmlNode = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); infoXml = infoXmlNode.toElement(); infoXml.setAttribute(QStringLiteral("upgraded"), 1); } m_doc.documentElement().setAttribute(QStringLiteral("upgraded"), 1); if (version <= 0.6) { QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders QDomNode westley = m_doc.elementsByTagName(QStringLiteral("westley")).at(1); QDomNode tractor = m_doc.elementsByTagName(QStringLiteral("tractor")).at(0); QDomNode multitrack = m_doc.elementsByTagName(QStringLiteral("multitrack")).at(0); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomNode props = m_doc.elementsByTagName(QStringLiteral("properties")).at(0).toElement(); QString profile = props.toElement().attribute(QStringLiteral("videoprofile")); int startPos = props.toElement().attribute(QStringLiteral("timeline_position")).toInt(); if (profile == QLatin1String("dv_wide")) { profile = QStringLiteral("dv_pal_wide"); } // move playlists outside of tractor and add the tracks instead int max = playlists.count(); if (westley.isNull()) { westley = m_doc.createElement(QStringLiteral("westley")); m_doc.documentElement().appendChild(westley); } if (tractor.isNull()) { // qCDebug(KDENLIVE_LOG) << "// NO MLT PLAYLIST, building empty one"; QDomElement blank_tractor = m_doc.createElement(QStringLiteral("tractor")); westley.appendChild(blank_tractor); QDomElement blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("black_track")); westley.insertBefore(blank_playlist, QDomNode()); QDomElement blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("black_track")); blank_tractor.appendChild(blank_track); QDomNodeList kdenlivetracks = m_doc.elementsByTagName(QStringLiteral("kdenlivetrack")); for (int i = 0; i < kdenlivetracks.count(); ++i) { blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("playlist") + QString::number(i)); westley.insertBefore(blank_playlist, QDomNode()); blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("playlist") + QString::number(i)); blank_tractor.appendChild(blank_track); if (kdenlivetracks.at(i).toElement().attribute(QStringLiteral("cliptype")) == QLatin1String("Sound")) { blank_playlist.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); blank_track.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); } } } else for (int i = 0; i < max; ++i) { QDomNode n = playlists.at(i); westley.insertBefore(n, QDomNode()); QDomElement pl = n.toElement(); QDomElement track = m_doc.createElement(QStringLiteral("track")); QString trackType = pl.attribute(QStringLiteral("hide")); if (!trackType.isEmpty()) { track.setAttribute(QStringLiteral("hide"), trackType); } QString playlist_id = pl.attribute(QStringLiteral("id")); if (playlist_id.isEmpty()) { playlist_id = QStringLiteral("black_track"); pl.setAttribute(QStringLiteral("id"), playlist_id); } track.setAttribute(QStringLiteral("producer"), playlist_id); // tractor.appendChild(track); #define KEEP_TRACK_ORDER 1 #ifdef KEEP_TRACK_ORDER tractor.insertAfter(track, QDomNode()); #else // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0 // insertion sort - O( tracks*tracks ) // Note, this breaks _all_ transitions - but you can move them up and down afterwards. QDomElement tractor_elem = tractor.toElement(); if (!tractor_elem.isNull()) { QDomNodeList tracks = tractor_elem.elementsByTagName("track"); int size = tracks.size(); if (size == 0) { tractor.insertAfter(track, QDomNode()); } else { bool inserted = false; for (int i = 0; i < size; ++i) { QDomElement track_elem = tracks.at(i).toElement(); if (track_elem.isNull()) { tractor.insertAfter(track, QDomNode()); inserted = true; break; } else { // qCDebug(KDENLIVE_LOG) << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer"); if (playlist_id < track_elem.attribute("producer")) { tractor.insertBefore(track, track_elem); inserted = true; break; } } } // Reach here, no insertion, insert last if (!inserted) { tractor.insertAfter(track, QDomNode()); } } } else { qCWarning(KDENLIVE_LOG) << "tractor was not a QDomElement"; tractor.insertAfter(track, QDomNode()); } #endif } tractor.removeChild(multitrack); // audio track mixing transitions should not be added to track view, so add required attribute QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement tr = transitions.at(i).toElement(); if (tr.attribute(QStringLiteral("combine")) == QLatin1String("1") && tr.attribute(QStringLiteral("mlt_service")) == QLatin1String("mix")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added")); QDomText value = m_doc.createTextNode(QStringLiteral("237")); property.appendChild(value); tr.appendChild(property); property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); value = m_doc.createTextNode(QStringLiteral("mix")); property.appendChild(value); tr.appendChild(property); } else { // convert transition QDomNamedNodeMap attrs = tr.attributes(); for (int j = 0; j < attrs.count(); ++j) { QString attrName = attrs.item(j).nodeName(); if (attrName != QLatin1String("in") && attrName != QLatin1String("out") && attrName != QLatin1String("id")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), attrName); QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue()); property.appendChild(value); tr.appendChild(property); } } } } // move transitions after tracks for (int i = 0; i < max; ++i) { tractor.insertAfter(transitions.at(0), QDomNode()); } // Fix filters format QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); max = entries.count(); for (int i = 0; i < max; ++i) { QString last_id; int effectix = 0; QDomNode m = entries.at(i).firstChild(); while (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("filter")) { QDomElement filt = m.toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute(QStringLiteral("kdenlive_id")); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive_ix")); QDomText value = m_doc.createTextNode(QString::number(effectix)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { // qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); auto property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), a.name()); auto property_value = m_doc.createTextNode(a.value()); property.appendChild(property_value); filt.appendChild(property); } } } m = m.nextSibling(); } } /* QDomNodeList filters = m_doc.elementsByTagName("filter"); max = filters.count(); QString last_id; int effectix = 0; for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute("kdenlive_id"); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement("property"); e.setAttribute("name", "kdenlive_ix"); QDomText value = m_doc.createTextNode(QString::number(1)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { //qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); QDomElement e = m_doc.createElement("property"); e.setAttribute("name", a.name()); QDomText value = m_doc.createTextNode(a.value()); e.appendChild(value); filt.appendChild(e); } } }*/ // fix slowmotion QDomNodeList producers = westley.toElement().elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.attribute(QStringLiteral("mlt_service")) == QLatin1String("framebuffer")) { QString slowmotionprod = prod.attribute(QStringLiteral("resource")); slowmotionprod.replace(QLatin1Char(':'), QLatin1Char('?')); // qCDebug(KDENLIVE_LOG) << "// FOUND WRONG SLOWMO, new: " << slowmotionprod; prod.setAttribute(QStringLiteral("resource"), slowmotionprod); } } // move producers to correct place, markers to a global list, fix clip descriptions QDomElement markers = m_doc.createElement(QStringLiteral("markers")); // This will get the xml producers: producers = m_doc.elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(0).toElement(); // add resource also as a property (to allow path correction in setNewResource()) // TODO: will it work with slowmotion? needs testing /*if (!prod.attribute("resource").isEmpty()) { QDomElement prop_resource = m_doc.createElement("property"); prop_resource.setAttribute("name", "resource"); QDomText resource = m_doc.createTextNode(prod.attribute("resource")); prop_resource.appendChild(resource); prod.appendChild(prop_resource); }*/ QDomNode m = prod.firstChild(); if (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("markers")) { QDomNodeList prodchilds = m.childNodes(); int maxchild = prodchilds.count(); for (int k = 0; k < maxchild; ++k) { QDomElement mark = prodchilds.at(0).toElement(); mark.setAttribute(QStringLiteral("id"), prod.attribute(QStringLiteral("id"))); markers.insertAfter(mark, QDomNode()); } prod.removeChild(m); - } else if (prod.attribute(QStringLiteral("type")).toInt() == Text) { + } else if (prod.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // convert title clip if (m.toElement().tagName() == QLatin1String("textclip")) { QDomDocument tdoc; QDomElement titleclip = m.toElement(); QDomElement title = tdoc.createElement(QStringLiteral("kdenlivetitle")); tdoc.appendChild(title); QDomNodeList objects = titleclip.childNodes(); int maxchild = objects.count(); for (int k = 0; k < maxchild; ++k) { QDomElement ob = objects.at(k).toElement(); if (ob.attribute(QStringLiteral("type")) == QLatin1String("3")) { // text object - all of this goes into "xmldata"... QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsTextItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); content.setAttribute(QStringLiteral("font"), ob.attribute(QStringLiteral("font_family"))); content.setAttribute(QStringLiteral("font-size"), ob.attribute(QStringLiteral("font_size"))); content.setAttribute(QStringLiteral("font-bold"), ob.attribute(QStringLiteral("bold"))); content.setAttribute(QStringLiteral("font-italic"), ob.attribute(QStringLiteral("italic"))); content.setAttribute(QStringLiteral("font-underline"), ob.attribute(QStringLiteral("underline"))); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("font-color"), colorToString(c)); // todo: These fields are missing from the newly generated xmldata: // transform, startviewport, endviewport, background QDomText conttxt = tdoc.createTextNode(ob.attribute(QStringLiteral("text"))); content.appendChild(conttxt); item.appendChild(position); item.appendChild(content); title.appendChild(item); } else if (ob.attribute(QStringLiteral("type")) == QLatin1String("5")) { // rectangle object QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsRectItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("brushcolor"), colorToString(c)); QString rect = QStringLiteral("0,0,"); rect.append(ob.attribute(QStringLiteral("width"))); rect.append(QLatin1String(",")); rect.append(ob.attribute(QStringLiteral("height"))); content.setAttribute(QStringLiteral("rect"), rect); item.appendChild(position); item.appendChild(content); title.appendChild(item); } } prod.setAttribute(QStringLiteral("xmldata"), tdoc.toString()); // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder()); // prod.setAttribute("titlename", titleInfo.at(0)); // prod.setAttribute("resource", titleInfo.at(1)); ////qCDebug(KDENLIVE_LOG)<<"TITLE DATA:\n"< 0) { prod.setAttribute(QStringLiteral("out"), QString::number(duration)); } // The clip goes back in, but text clips should not go back in, at least not modified westley.insertBefore(prod, QDomNode()); } QDomNode westley0 = m_doc.elementsByTagName(QStringLiteral("westley")).at(0); if (!markers.firstChild().isNull()) { westley0.appendChild(markers); } /* * Convert as much of the kdenlivedoc as possible. Use the producer in * westley. First, remove the old stuff from westley, and add a new * empty one. Also, track the max id in order to use it for the adding * of groups/folders */ int max_kproducer_id = 0; westley0.removeChild(infoXmlNode); QDomElement infoXml_new = m_doc.createElement(QStringLiteral("kdenlivedoc")); infoXml_new.setAttribute(QStringLiteral("profile"), profile); infoXml.setAttribute(QStringLiteral("position"), startPos); // Add all the producers that has a resource in westley QDomElement westley_element = westley0.toElement(); if (westley_element.isNull()) { qCWarning(KDENLIVE_LOG) << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc"; } else { QDomNodeList wproducers = westley_element.elementsByTagName(QStringLiteral("producer")); int kmax = wproducers.count(); for (int i = 0; i < kmax; ++i) { QDomElement wproducer = wproducers.at(i).toElement(); if (wproducer.isNull()) { qCWarning(KDENLIVE_LOG) << "Found producer in westley0, that was not a QDomElement"; continue; } if (wproducer.attribute(QStringLiteral("id")) == QLatin1String("black")) { continue; } // We have to do slightly different things, depending on the type // qCDebug(KDENLIVE_LOG) << "Converting producer element with type" << wproducer.attribute("type"); - if (wproducer.attribute(QStringLiteral("type")).toInt() == Text) { + if (wproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // qCDebug(KDENLIVE_LOG) << "Found TEXT element in producer" << endl; QDomElement kproducer = wproducer.cloneNode(true).toElement(); kproducer.setTagName(QStringLiteral("kdenlive_producer")); infoXml_new.appendChild(kproducer); /* * TODO: Perhaps needs some more changes here to * "frequency", aspect ratio as a float, frame_size, * channels, and later, resource and title name */ } else { QDomElement kproducer = m_doc.createElement(QStringLiteral("kdenlive_producer")); kproducer.setAttribute(QStringLiteral("id"), wproducer.attribute(QStringLiteral("id"))); if (!wproducer.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), wproducer.attribute(QStringLiteral("description"))); } kproducer.setAttribute(QStringLiteral("resource"), wproducer.attribute(QStringLiteral("resource"))); kproducer.setAttribute(QStringLiteral("type"), wproducer.attribute(QStringLiteral("type"))); // Testing fix for 358 if (!wproducer.attribute(QStringLiteral("aspect_ratio")).isEmpty()) { kproducer.setAttribute(QStringLiteral("aspect_ratio"), wproducer.attribute(QStringLiteral("aspect_ratio"))); } if (!wproducer.attribute(QStringLiteral("source_fps")).isEmpty()) { kproducer.setAttribute(QStringLiteral("fps"), wproducer.attribute(QStringLiteral("source_fps"))); } if (!wproducer.attribute(QStringLiteral("length")).isEmpty()) { kproducer.setAttribute(QStringLiteral("duration"), wproducer.attribute(QStringLiteral("length"))); } infoXml_new.appendChild(kproducer); } if (wproducer.attribute(QStringLiteral("id")).toInt() > max_kproducer_id) { max_kproducer_id = wproducer.attribute(QStringLiteral("id")).toInt(); } } } #define LOOKUP_FOLDER 1 #ifdef LOOKUP_FOLDER /* * Look through all the folder elements of the old doc, for each folder, * for each producer, get the id, look it up in the new doc, set the * groupname and groupid. Note, this does not work at the moment - at * least one folder shows up missing, and clips with no folder does not * show up. */ // QDomElement infoXml_old = infoXmlNode.toElement(); if (!infoXml_old.isNull()) { QDomNodeList folders = infoXml_old.elementsByTagName(QStringLiteral("folder")); int fsize = folders.size(); int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers for (int i = 0; i < fsize; ++i) { QDomElement folder = folders.at(i).toElement(); if (!folder.isNull()) { QString groupName = folder.attribute(QStringLiteral("name")); // qCDebug(KDENLIVE_LOG) << "groupName: " << groupName << " with groupId: " << groupId; QDomNodeList fproducers = folder.elementsByTagName(QStringLiteral("producer")); int psize = fproducers.size(); for (int j = 0; j < psize; ++j) { QDomElement fproducer = fproducers.at(j).toElement(); if (!fproducer.isNull()) { QString id = fproducer.attribute(QStringLiteral("id")); // This is not very effective, but compared to loading the clips, its a breeze QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName(QStringLiteral("kdenlive_producer")); int kpsize = kdenlive_producers.size(); for (int k = 0; k < kpsize; ++k) { QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure if (id == kproducer.attribute(QStringLiteral("id"))) { // We do not check that it already is part of a folder kproducer.setAttribute(QStringLiteral("groupid"), groupId); kproducer.setAttribute(QStringLiteral("groupname"), groupName); break; } } } } ++groupId; } } } #endif QDomNodeList elements = westley.childNodes(); max = elements.count(); for (int i = 0; i < max; ++i) { QDomElement prod = elements.at(0).toElement(); westley0.insertAfter(prod, QDomNode()); } westley0.appendChild(infoXml_new); westley0.removeChild(westley); // adds information to QDomNodeList kproducers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); QDomNodeList avfiles = infoXml_old.elementsByTagName(QStringLiteral("avfile")); // qCDebug(KDENLIVE_LOG) << "found" << avfiles.count() << "s and" << kproducers.count() << "s"; for (int i = 0; i < avfiles.count(); ++i) { QDomElement avfile = avfiles.at(i).toElement(); QDomElement kproducer; if (avfile.isNull()) { qCWarning(KDENLIVE_LOG) << "found an that is not a QDomElement"; } else { QString id = avfile.attribute(QStringLiteral("id")); // this is horrible, must be rewritten, it's just for test for (int j = 0; j < kproducers.count(); ++j) { ////qCDebug(KDENLIVE_LOG) << "checking with id" << kproducers.at(j).toElement().attribute("id"); if (kproducers.at(j).toElement().attribute(QStringLiteral("id")) == id) { kproducer = kproducers.at(j).toElement(); break; } } if (kproducer == QDomElement()) { qCWarning(KDENLIVE_LOG) << "no match for with id =" << id; } else { ////qCDebug(KDENLIVE_LOG) << "ready to set additional 's attributes (id =" << id << ')'; kproducer.setAttribute(QStringLiteral("channels"), avfile.attribute(QStringLiteral("channels"))); kproducer.setAttribute(QStringLiteral("duration"), avfile.attribute(QStringLiteral("duration"))); kproducer.setAttribute(QStringLiteral("frame_size"), avfile.attribute(QStringLiteral("width")) + QLatin1Char('x') + avfile.attribute(QStringLiteral("height"))); kproducer.setAttribute(QStringLiteral("frequency"), avfile.attribute(QStringLiteral("frequency"))); if (kproducer.attribute(QStringLiteral("description")).isEmpty() && !avfile.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), avfile.attribute(QStringLiteral("description"))); } } } } infoXml = infoXml_new; } if (version <= 0.81) { // Add the tracks information QString tracksOrder = infoXml.attribute(QStringLiteral("tracks")); if (tracksOrder.isEmpty()) { QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); ++i) { QDomElement track = tracks.at(i).toElement(); if (track.attribute(QStringLiteral("producer")) != QLatin1String("black_track")) { if (track.attribute(QStringLiteral("hide")) == QLatin1String("video")) { tracksOrder.append(QLatin1Char('a')); } else { tracksOrder.append(QLatin1Char('v')); } } } } QDomElement tracksinfo = m_doc.createElement(QStringLiteral("tracksinfo")); for (int i = 0; i < tracksOrder.size(); ++i) { QDomElement trackinfo = m_doc.createElement(QStringLiteral("trackinfo")); if (tracksOrder.data()[i] == QLatin1Char('a')) { trackinfo.setAttribute(QStringLiteral("type"), QStringLiteral("audio")); trackinfo.setAttribute(QStringLiteral("blind"), 1); } else { trackinfo.setAttribute(QStringLiteral("blind"), 0); } trackinfo.setAttribute(QStringLiteral("mute"), 0); trackinfo.setAttribute(QStringLiteral("locked"), 0); tracksinfo.appendChild(trackinfo); } infoXml.appendChild(tracksinfo); } if (version <= 0.82) { // Convert s in s (MLT extreme makeover) QDomNodeList westleyNodes = m_doc.elementsByTagName(QStringLiteral("westley")); for (int i = 0; i < westleyNodes.count(); ++i) { QDomElement westley = westleyNodes.at(i).toElement(); westley.setTagName(QStringLiteral("mlt")); } } if (version <= 0.83) { // Replace point size with pixel size in text titles if (m_doc.toString().contains(QStringLiteral("font-size"))) { KMessageBox::ButtonCode convert = KMessageBox::Continue; QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); - if (kproducer.attribute(QStringLiteral("type")).toInt() == Text) { + if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QDomDocument data; data.setContent(kproducer.attribute(QStringLiteral("xmldata"))); QDomNodeList items = data.firstChild().childNodes(); for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) { if (items.at(j).attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsTextItem")) { QDomNamedNodeMap textProperties = items.at(j).namedItem(QStringLiteral("content")).attributes(); if (textProperties.namedItem(QStringLiteral("font-pixel-size")).isNull() && !textProperties.namedItem(QStringLiteral("font-size")).isNull()) { // Ask the user if he wants to convert if (convert != KMessageBox::Yes && convert != KMessageBox::No) { convert = (KMessageBox::ButtonCode)KMessageBox::warningYesNo( QApplication::activeWindow(), i18n("Some of your text clips were saved with size in points, which means different sizes on different displays. Do " "you want to convert them to pixel size, making them portable? It is recommended you do this on the computer they " "were first created on, or you could have to adjust their size."), i18n("Update Text Clips")); } if (convert == KMessageBox::Yes) { QFont font; font.setPointSize(textProperties.namedItem(QStringLiteral("font-size")).nodeValue().toInt()); QDomElement content = items.at(j).namedItem(QStringLiteral("content")).toElement(); content.setAttribute(QStringLiteral("font-pixel-size"), QFontInfo(font).pixelSize()); content.removeAttribute(QStringLiteral("font-size")); kproducer.setAttribute(QStringLiteral("xmldata"), data.toString()); /* * You may be tempted to delete the preview file * to force its recreation: bad idea (see * http://www.kdenlive.org/mantis/view.php?id=749) */ } } } } } } } // Fill the element QDomElement docProperties = infoXml.firstChildElement(QStringLiteral("documentproperties")); if (docProperties.isNull()) { docProperties = m_doc.createElement(QStringLiteral("documentproperties")); docProperties.setAttribute(QStringLiteral("zonein"), infoXml.attribute(QStringLiteral("zonein"))); docProperties.setAttribute(QStringLiteral("zoneout"), infoXml.attribute(QStringLiteral("zoneout"))); docProperties.setAttribute(QStringLiteral("zoom"), infoXml.attribute(QStringLiteral("zoom"))); docProperties.setAttribute(QStringLiteral("position"), infoXml.attribute(QStringLiteral("position"))); infoXml.appendChild(docProperties); } } if (version <= 0.84) { // update the title clips to use the new MLT kdenlivetitle producer QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count(); ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); - if (kproducer.attribute(QStringLiteral("type")).toInt() == Text) { + if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QString data = kproducer.attribute(QStringLiteral("xmldata")); QString datafile = kproducer.attribute(QStringLiteral("resource")); if (!datafile.endsWith(QLatin1String(".kdenlivetitle"))) { datafile = QString(); kproducer.setAttribute(QStringLiteral("resource"), QString()); } QString id = kproducer.attribute(QStringLiteral("id")); QDomNodeList mltproducers = m_doc.elementsByTagName(QStringLiteral("producer")); bool foundData = false; bool foundResource = false; bool foundService = false; for (int j = 0; j < mltproducers.count(); ++j) { QDomElement wproducer = mltproducers.at(j).toElement(); if (wproducer.attribute(QStringLiteral("id")) == id) { QDomNodeList props = wproducer.childNodes(); for (int k = 0; k < props.count(); ++k) { if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { props.at(k).firstChild().setNodeValue(data); foundData = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("mlt_service")) { props.at(k).firstChild().setNodeValue(QStringLiteral("kdenlivetitle")); foundService = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("resource")) { props.at(k).firstChild().setNodeValue(datafile); foundResource = true; } } if (!foundData) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("xmldata")); QDomText value = m_doc.createTextNode(data); e.appendChild(value); wproducer.appendChild(e); } if (!foundService) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText value = m_doc.createTextNode(QStringLiteral("kdenlivetitle")); e.appendChild(value); wproducer.appendChild(e); } if (!foundResource) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText value = m_doc.createTextNode(datafile); e.appendChild(value); wproducer.appendChild(e); } break; } } } } } if (version <= 0.85) { // update the LADSPA effects to use the new ladspa.id format instead of external xml file QDomNodeList effectNodes = m_doc.elementsByTagName(QStringLiteral("filter")); for (int i = 0; i < effectNodes.count(); ++i) { QDomElement effect = effectNodes.at(i).toElement(); if (EffectsList::property(effect, QStringLiteral("mlt_service")) == QLatin1String("ladspa")) { // Needs to be converted QStringList info = getInfoFromEffectName(EffectsList::property(effect, QStringLiteral("kdenlive_id"))); if (info.isEmpty()) { continue; } // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names EffectsList::setProperty(effect, QStringLiteral("kdenlive_id"), info.at(0)); EffectsList::setProperty(effect, QStringLiteral("tag"), info.at(0)); EffectsList::setProperty(effect, QStringLiteral("mlt_service"), info.at(0)); EffectsList::removeProperty(effect, QStringLiteral("src")); for (int j = 1; j < info.size(); ++j) { QString value = EffectsList::property(effect, info.at(j).section(QLatin1Char('='), 0, 0)); if (!value.isEmpty()) { // update parameter name EffectsList::renameProperty(effect, info.at(j).section(QLatin1Char('='), 0, 0), info.at(j).section(QLatin1Char('='), 1, 1)); } } } } } if (version <= 0.86) { // Make sure we don't have avformat-novalidate producers, since it caused crashes QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (EffectsList::property(prod, QStringLiteral("mlt_service")) == QLatin1String("avformat-novalidate")) { EffectsList::setProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("avformat")); } } // There was a mistake in Geometry transitions where the last keyframe was created one frame after the end of transition, so fix it and move last // keyframe to real end of transition // Get profile info (width / height) int profileWidth; int profileHeight; QDomElement profile = m_doc.firstChildElement(QStringLiteral("profile")); if (profile.isNull()) { profile = infoXml.firstChildElement(QStringLiteral("profileinfo")); if (!profile.isNull()) { // old MLT format, we need to add profile QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomElement pr = profile.cloneNode().toElement(); pr.setTagName(QStringLiteral("profile")); mlt.insertBefore(pr, firstProd); } } if (profile.isNull()) { // could not find profile info, set PAL profileWidth = 720; profileHeight = 576; } else { profileWidth = profile.attribute(QStringLiteral("width")).toInt(); profileHeight = profile.attribute(QStringLiteral("height")).toInt(); } QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement trans = transitions.at(i).toElement(); int out = trans.attribute(QStringLiteral("out")).toInt() - trans.attribute(QStringLiteral("in")).toInt(); QString geom = EffectsList::property(trans, QStringLiteral("geometry")); Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight); Mlt::GeometryItem item; if (g->next_key(&item, out) == 0) { // We have a keyframe just after last frame, try to move it to last frame if (item.frame() == out + 1) { item.frame(out); g->insert(item); g->remove(out + 1); EffectsList::setProperty(trans, QStringLiteral("geometry"), QString::fromLatin1(g->serialise())); } } delete g; } } if (version <= 0.87) { if (!m_doc.firstChildElement(QStringLiteral("mlt")).hasAttribute(QStringLiteral("LC_NUMERIC"))) { m_doc.firstChildElement(QStringLiteral("mlt")).setAttribute(QStringLiteral("LC_NUMERIC"), QStringLiteral("C")); } } if (version <= 0.88) { // convert to new MLT-only format QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomDocumentFragment frag = m_doc.createDocumentFragment(); // Create Bin Playlist QDomElement main_playlist = m_doc.createElement(QStringLiteral("playlist")); QDomElement prop = m_doc.createElement(QStringLiteral("property")); prop.setAttribute(QStringLiteral("name"), QStringLiteral("xml_retain")); QDomText val = m_doc.createTextNode(QStringLiteral("1")); prop.appendChild(val); main_playlist.appendChild(prop); // Move markers QDomNodeList markers = m_doc.elementsByTagName(QStringLiteral("marker")); for (int i = 0; i < markers.count(); ++i) { QDomElement marker = markers.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:marker.") + marker.attribute(QStringLiteral("id")) + QLatin1Char(':') + marker.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(marker.attribute(QStringLiteral("type")) + QLatin1Char(':') + marker.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move guides QDomNodeList guides = m_doc.elementsByTagName(QStringLiteral("guide")); for (int i = 0; i < guides.count(); ++i) { QDomElement guide = guides.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:guide.") + guide.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(guide.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move folders QDomNodeList folders = m_doc.elementsByTagName(QStringLiteral("folder")); for (int i = 0; i < folders.count(); ++i) { QDomElement folder = folders.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folder.-1.") + folder.attribute(QStringLiteral("id"))); QDomText val_node = m_doc.createTextNode(folder.attribute(QStringLiteral("name"))); property.appendChild(val_node); main_playlist.appendChild(property); } QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); main_playlist.setAttribute(QStringLiteral("id"), pCore->binController()->binPlaylistId()); mlt.toElement().setAttribute(QStringLiteral("producer"), pCore->binController()->binPlaylistId()); QStringList ids; QStringList slowmotionIds; QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomNodeList kdenlive_producers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); // Rename all track producers to correct name: "id_playlistName" instead of "id_trackNumber" QMap trackRenaming; // Create a list of which producers / track on which the producer is QMap playlistForId; QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomElement entry = entries.at(i).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == QLatin1String("black")) { continue; } bool audioOnlyProducer = false; if (trackRenaming.contains(entryId)) { // rename entry.setAttribute(QStringLiteral("producer"), trackRenaming.value(entryId)); continue; } if (entryId.endsWith(QLatin1String("_video"))) { // Video only producers are not track aware continue; } if (entryId.endsWith(QLatin1String("_audio"))) { // Audio only producer audioOnlyProducer = true; entryId = entryId.section(QLatin1Char('_'), 0, -2); } if (!entryId.contains(QLatin1Char('_'))) { // not a track producer playlistForId.insert(entryId, entry.parentNode().toElement().attribute(QStringLiteral("id"))); continue; } if (entryId.startsWith(QLatin1String("slowmotion:"))) { // Check broken slowmotion producers (they should not be track aware) QString newId = QStringLiteral("slowmotion:") + entryId.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0) + QLatin1Char(':') + entryId.section(QLatin1Char(':'), 2); trackRenaming.insert(entryId, newId); entry.setAttribute(QStringLiteral("producer"), newId); continue; } QString track = entryId.section(QLatin1Char('_'), 1, 1); QString playlistId = entry.parentNode().toElement().attribute(QStringLiteral("id")); if (track == playlistId) { continue; } QString newId = entryId.section(QLatin1Char('_'), 0, 0) + QLatin1Char('_') + playlistId; if (audioOnlyProducer) { newId.append(QStringLiteral("_audio")); trackRenaming.insert(entryId + QStringLiteral("_audio"), newId); } else { trackRenaming.insert(entryId, newId); } entry.setAttribute(QStringLiteral("producer"), newId); } if (!trackRenaming.isEmpty()) { for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (trackRenaming.contains(id)) { prod.setAttribute(QStringLiteral("id"), trackRenaming.value(id)); } } } // Create easily searchable index of original producers QMap m_source_producers; for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); m_source_producers.insert(id, prod); } for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == QLatin1String("black")) { continue; } if (id.startsWith(QLatin1String("slowmotion"))) { // No need to process slowmotion producers QString slowmo = id.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0); if (!slowmotionIds.contains(slowmo)) { slowmotionIds << slowmo; } continue; } QString prodId = id.section(QLatin1Char('_'), 0, 0); if (ids.contains(prodId)) { // Make sure we didn't create a duplicate if (ids.contains(id)) { // we have a duplicate, check if this needs to be a track producer QString service = EffectsList::property(prod, QStringLiteral("mlt_service")); int a_ix = EffectsList::property(prod, QStringLiteral("audio_index")).toInt(); if (service == QLatin1String("xml") || service == QLatin1String("consumer") || (service.contains(QStringLiteral("avformat")) && a_ix != -1)) { // This should be a track producer, rename QString newId = id + QLatin1Char('_') + playlistForId.value(id); prod.setAttribute(QStringLiteral("id"), newId); for (int j = 0; j < entries.count(); j++) { QDomElement entry = entries.at(j).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == id) { entry.setAttribute(QStringLiteral("producer"), newId); } } } else { // This is a duplicate, remove mlt.removeChild(prod); i--; } } // Already processed, continue continue; } if (id == prodId) { // This is an original producer, move it to the main playlist QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); QString service = EffectsList::property(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("kdenlivetitle")) { fixTitleProducerLocale(prod); } QDomElement source = m_source_producers.value(id); if (!source.isNull()) { updateProducerInfo(prod, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(prod); // Changing prod parent removes it from list, so rewind index i--; } else { QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setAttribute(QStringLiteral("id"), prodId); if (id.endsWith(QLatin1String("_audio"))) { EffectsList::removeProperty(originalProd, QStringLiteral("video_index")); } else if (id.endsWith(QLatin1String("_video"))) { EffectsList::removeProperty(originalProd, QStringLiteral("audio_index")); } QDomElement source = m_source_producers.value(prodId); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); if (!source.isNull()) { updateProducerInfo(originalProd, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(originalProd); entry.setAttribute(QStringLiteral("producer"), prodId); main_playlist.appendChild(entry); } ids.append(prodId); } // Make sure to include producers that were not in timeline for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (!ids.contains(id)) { // Clip was not in timeline, create it QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setTagName(QStringLiteral("producer")); EffectsList::setProperty(originalProd, QStringLiteral("resource"), originalProd.attribute(QStringLiteral("resource"))); updateProducerInfo(originalProd, prod); originalProd.removeAttribute(QStringLiteral("proxy")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("file_hash")); originalProd.removeAttribute(QStringLiteral("file_size")); originalProd.removeAttribute(QStringLiteral("frame_size")); originalProd.removeAttribute(QStringLiteral("proxy_out")); originalProd.removeAttribute(QStringLiteral("zone_out")); originalProd.removeAttribute(QStringLiteral("zone_in")); originalProd.removeAttribute(QStringLiteral("name")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("duration")); originalProd.removeAttribute(QStringLiteral("cutzones")); int type = prod.attribute(QStringLiteral("type")).toInt(); QString mltService; switch (type) { case 4: mltService = QStringLiteral("colour"); break; case 5: case 7: mltService = QStringLiteral("qimage"); break; case 6: mltService = QStringLiteral("kdenlivetitle"); break; case 9: mltService = QStringLiteral("xml"); break; default: mltService = QStringLiteral("avformat"); break; } EffectsList::setProperty(originalProd, QStringLiteral("mlt_service"), mltService); EffectsList::setProperty(originalProd, QStringLiteral("mlt_type"), QStringLiteral("producer")); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(prod.attribute(QStringLiteral("duration")).toInt() - 1)); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); if (type == 6) { fixTitleProducerLocale(originalProd); } frag.appendChild(originalProd); ids << id; } } // Set clip folders for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); QString folder = prod.attribute(QStringLiteral("groupid")); QDomNodeList mlt_producers = frag.childNodes(); for (int k = 0; k < mlt_producers.count(); k++) { QDomElement mltprod = mlt_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("producer")) { continue; } if (mltprod.attribute(QStringLiteral("id")) == id) { if (!folder.isEmpty()) { // We have found our producer, set folder info QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folderid")); QDomText val_node = m_doc.createTextNode(folder); property.appendChild(val_node); mltprod.appendChild(property); } break; } } } // Make sure all slowmotion producers have a master clip for (int i = 0; i < slowmotionIds.count(); i++) { const QString &slo = slowmotionIds.at(i); if (!ids.contains(slo)) { // rebuild producer from Kdenlive's old xml format for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == slo) { // We found the kdenlive_producer, build MLT producer QDomElement original = m_doc.createElement(QStringLiteral("producer")); original.setAttribute(QStringLiteral("in"), 0); original.setAttribute(QStringLiteral("out"), prod.attribute(QStringLiteral("duration")).toInt() - 1); original.setAttribute(QStringLiteral("id"), id); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText val_node = m_doc.createTextNode(prod.attribute(QStringLiteral("resource"))); property.appendChild(val_node); original.appendChild(property); QDomElement prop2 = m_doc.createElement(QStringLiteral("property")); prop2.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText val2 = m_doc.createTextNode(QStringLiteral("avformat")); prop2.appendChild(val2); original.appendChild(prop2); QDomElement prop3 = m_doc.createElement(QStringLiteral("property")); prop3.setAttribute(QStringLiteral("name"), QStringLiteral("length")); QDomText val3 = m_doc.createTextNode(prod.attribute(QStringLiteral("duration"))); prop3.appendChild(val3); original.appendChild(prop3); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), original.attribute(QStringLiteral("in"))); entry.setAttribute(QStringLiteral("out"), original.attribute(QStringLiteral("out"))); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); frag.appendChild(original); ids << slo; break; } } } } frag.appendChild(main_playlist); mlt.insertBefore(frag, firstProd); } if (version < 0.91) { // Migrate track properties QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNodeList old_tracks = m_doc.elementsByTagName(QStringLiteral("trackinfo")); QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < old_tracks.count(); i++) { QString playlistName = tracks.at(i + 1).toElement().attribute(QStringLiteral("producer")); // find playlist for track QDomElement trackPlaylist; for (int j = 0; j < playlists.count(); j++) { if (playlists.at(j).toElement().attribute(QStringLiteral("id")) == playlistName) { trackPlaylist = playlists.at(j).toElement(); break; } } if (!trackPlaylist.isNull()) { QDomElement kdenliveTrack = old_tracks.at(i).toElement(); if (kdenliveTrack.attribute(QStringLiteral("type")) == QLatin1String("audio")) { EffectsList::setProperty(trackPlaylist, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); } if (kdenliveTrack.attribute(QStringLiteral("locked")) == QLatin1String("1")) { EffectsList::setProperty(trackPlaylist, QStringLiteral("kdenlive:locked_track"), QStringLiteral("1")); } EffectsList::setProperty(trackPlaylist, QStringLiteral("kdenlive:track_name"), kdenliveTrack.attribute(QStringLiteral("trackname"))); } } // Find bin playlist playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomElement playlist; for (int i = 0; i < playlists.count(); i++) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == pCore->binController()->binPlaylistId()) { playlist = playlists.at(i).toElement(); break; } } // Migrate document notes QDomNodeList notesList = m_doc.elementsByTagName(QStringLiteral("documentnotes")); if (!notesList.isEmpty()) { QDomElement notes_elem = notesList.at(0).toElement(); QString notes = notes_elem.firstChild().nodeValue(); EffectsList::setProperty(playlist, QStringLiteral("kdenlive:documentnotes"), notes); } // Migrate clip groups QDomNodeList groupElement = m_doc.elementsByTagName(QStringLiteral("groups")); if (!groupElement.isEmpty()) { QDomElement groups = groupElement.at(0).toElement(); QDomDocument d2; d2.importNode(groups, true); EffectsList::setProperty(playlist, QStringLiteral("kdenlive:clipgroups"), d2.toString()); } // Migrate custom effects QDomNodeList effectsElement = m_doc.elementsByTagName(QStringLiteral("customeffects")); if (!effectsElement.isEmpty()) { QDomElement effects = effectsElement.at(0).toElement(); QDomDocument d2; d2.importNode(effects, true); EffectsList::setProperty(playlist, QStringLiteral("kdenlive:customeffects"), d2.toString()); } EffectsList::setProperty(playlist, QStringLiteral("kdenlive:docproperties.version"), QString::number(currentVersion)); if (!infoXml.isNull()) { EffectsList::setProperty(playlist, QStringLiteral("kdenlive:docproperties.projectfolder"), infoXml.attribute(QStringLiteral("projectfolder"))); } // Remove deprecated Kdenlive extra info from xml doc before sending it to MLT QDomElement docXml = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); if (!docXml.isNull()) { mlt.removeChild(docXml); } } if (version < 0.92) { // Luma transition used for wipe is deprecated, we now use a composite, convert QDomNodeList transitionList = m_doc.elementsByTagName(QStringLiteral("transition")); QDomElement trans; for (int i = 0; i < transitionList.count(); i++) { trans = transitionList.at(i).toElement(); QString id = EffectsList::property(trans, QStringLiteral("kdenlive_id")); if (id == QLatin1String("luma")) { EffectsList::setProperty(trans, QStringLiteral("kdenlive_id"), QStringLiteral("wipe")); EffectsList::setProperty(trans, QStringLiteral("mlt_service"), QStringLiteral("composite")); bool reverse = EffectsList::property(trans, QStringLiteral("reverse")).toInt() != 0; EffectsList::setProperty(trans, QStringLiteral("luma_invert"), EffectsList::property(trans, QStringLiteral("invert"))); EffectsList::setProperty(trans, QStringLiteral("luma"), EffectsList::property(trans, QStringLiteral("resource"))); EffectsList::removeProperty(trans, QStringLiteral("invert")); EffectsList::removeProperty(trans, QStringLiteral("reverse")); EffectsList::removeProperty(trans, QStringLiteral("resource")); if (reverse) { EffectsList::setProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0")); } else { EffectsList::setProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:0;-1=0%/0%:100%x100%:100")); } EffectsList::setProperty(trans, QStringLiteral("aligned"), QStringLiteral("0")); EffectsList::setProperty(trans, QStringLiteral("fill"), QStringLiteral("1")); } } } if (version < 0.93) { // convert old keyframe filters to animated // these filters were "animated" by adding several instance of the filter, each one having a start and end tag. // We convert by parsing the start and end tags vor values and adding all to the new animated parameter QMap keyframeFilterToConvert; keyframeFilterToConvert.insert(QStringLiteral("volume"), QStringList() << QStringLiteral("gain") << QStringLiteral("end") << QStringLiteral("level")); keyframeFilterToConvert.insert(QStringLiteral("brightness"), QStringList() << QStringLiteral("start") << QStringLiteral("end") << QStringLiteral("level")); QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomNode entry = entries.at(i); QDomNodeList effects = entry.toElement().elementsByTagName(QStringLiteral("filter")); QStringList parsedIds; for (int j = 0; j < effects.count(); j++) { QDomElement eff = effects.at(j).toElement(); QString id = EffectsList::property(eff, QStringLiteral("kdenlive_id")); if (keyframeFilterToConvert.contains(id) && !parsedIds.contains(id)) { parsedIds << id; QMap values; QStringList conversionParams = keyframeFilterToConvert.value(id); int offset = eff.attribute(QStringLiteral("in")).toInt(); int out = eff.attribute(QStringLiteral("out")).toInt(); convertKeyframeEffect(eff, conversionParams, values, offset); EffectsList::removeProperty(eff, conversionParams.at(0)); EffectsList::removeProperty(eff, conversionParams.at(1)); for (int k = j + 1; k < effects.count(); k++) { QDomElement subEffect = effects.at(k).toElement(); QString subId = EffectsList::property(subEffect, QStringLiteral("kdenlive_id")); if (subId == id) { convertKeyframeEffect(subEffect, conversionParams, values, offset); out = subEffect.attribute(QStringLiteral("out")).toInt(); entry.removeChild(subEffect); k--; } } QStringList parsedValues; QLocale locale; QMapIterator l(values); if (id == QLatin1String("volume")) { // convert old volume range (0-300) to new dB values (-60-60) while (l.hasNext()) { l.next(); double v = l.value(); if (v <= 0) { v = -60; } else { v = log10(v) * 20; } parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(v); } } else { while (l.hasNext()) { l.next(); parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(l.value()); } } EffectsList::setProperty(eff, conversionParams.at(2), parsedValues.join(QLatin1Char(';'))); // EffectsList::setProperty(eff, QStringLiteral("kdenlive:sync_in_out"), QStringLiteral("1")); eff.setAttribute(QStringLiteral("out"), out); } } } } if (version < 0.94) { // convert slowmotion effects/producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList slowmoIds; for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id.startsWith(QLatin1String("slowmotion"))) { QString service = EffectsList::property(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("framebuffer")) { // convert to new timewarp producer prod.setAttribute(QStringLiteral("id"), id + QStringLiteral(":1")); slowmoIds << id; EffectsList::setProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("timewarp")); QString resource = EffectsList::property(prod, QStringLiteral("resource")); EffectsList::setProperty(prod, QStringLiteral("warp_resource"), resource.section(QLatin1Char('?'), 0, 0)); EffectsList::setProperty(prod, QStringLiteral("warp_speed"), resource.section(QLatin1Char('?'), 1).section(QLatin1Char(':'), 0, 0)); EffectsList::setProperty(prod, QStringLiteral("resource"), resource.section(QLatin1Char('?'), 1) + QLatin1Char(':') + resource.section(QLatin1Char('?'), 0, 0)); EffectsList::setProperty(prod, QStringLiteral("audio_index"), QStringLiteral("-1")); } } } if (!slowmoIds.isEmpty()) { producers = m_doc.elementsByTagName(QStringLiteral("entry")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString entryId = prod.attribute(QStringLiteral("producer")); if (slowmoIds.contains(entryId)) { prod.setAttribute(QStringLiteral("producer"), entryId + QStringLiteral(":1")); } } } // qCDebug(KDENLIVE_LOG)<<"------------------------\n"< markersList; for (int i = 0; i < props.count(); ++i) { QDomNode n = props.at(i); QString prop = n.toElement().attribute(QStringLiteral("name")); if (prop.startsWith(QLatin1String("kdenlive:guide."))) { //Process guide double guidePos = prop.section(QLatin1Char('.'), 1).toDouble(); QJsonObject currentGuide; currentGuide.insert(QStringLiteral("pos"), QJsonValue(GenTime(guidePos).frames(pCore->getCurrentFps()))); currentGuide.insert(QStringLiteral("comment"), QJsonValue(n.firstChild().nodeValue())); currentGuide.insert(QStringLiteral("type"), QJsonValue(0)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); guidesList.push_back(currentGuide); } else if (prop.startsWith(QLatin1String("kdenlive:marker."))) { //Process marker double markerPos = prop.section(QLatin1Char(':'), -1).toDouble(); QString markerBinClip = prop.section(QLatin1Char('.'), 1).section(QLatin1Char(':'), 0, 0); QString markerData = n.firstChild().nodeValue(); int markerType = markerData.section(QLatin1Char(':'), 0, 0).toInt(); QString markerComment = markerData.section(QLatin1Char(':'), 1); QJsonObject currentMarker; currentMarker.insert(QStringLiteral("pos"), QJsonValue(GenTime(markerPos).frames(pCore->getCurrentFps()))); currentMarker.insert(QStringLiteral("comment"), QJsonValue(markerComment)); currentMarker.insert(QStringLiteral("type"), QJsonValue(markerType)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); if (markersList.contains(markerBinClip)) { // we already have a marker list for this clip QJsonArray markerList = markersList.value(markerBinClip); markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } else { QJsonArray markerList; markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } } } if (!guidesList.isEmpty()) { QJsonDocument json(guidesList); EffectsList::setProperty(main_playlist, QStringLiteral("kdenlive:docproperties.guides"), json.toJson()); } // Update producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) continue; // Move to new kdenlive:id format const QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); EffectsList::setProperty(prod, QStringLiteral("kdenlive:id"), id); if (markersList.contains(id)) { QJsonDocument json(markersList.value(id)); EffectsList::setProperty(prod, QStringLiteral("kdenlive:markers"), json.toJson()); } // Check image sequences with buggy begin frame number const QString service = EffectsList::property(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("pixbuf") || service == QLatin1String("qimage")) { QString resource = EffectsList::property(prod, QStringLiteral("resource")); if (resource.contains(QStringLiteral("?begin:"))) { resource.replace(QStringLiteral("?begin:"), QStringLiteral("?begin=")); EffectsList::setProperty(prod, QStringLiteral("resource"), resource); } } } } m_modified = true; return true; } void DocumentValidator::convertKeyframeEffect(const QDomElement &effect, const QStringList ¶ms, QMap &values, int offset) { QLocale locale; int in = effect.attribute(QStringLiteral("in")).toInt() - offset; values.insert(in, locale.toDouble(EffectsList::property(effect, params.at(0)))); QString endValue = EffectsList::property(effect, params.at(1)); if (!endValue.isEmpty()) { int out = effect.attribute(QStringLiteral("out")).toInt() - offset; values.insert(out, locale.toDouble(endValue)); } } void DocumentValidator::updateProducerInfo(const QDomElement &prod, const QDomElement &source) { QString pxy = source.attribute(QStringLiteral("proxy")); if (pxy.length() > 1) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:proxy"), pxy); EffectsList::setProperty(prod, QStringLiteral("kdenlive:originalurl"), source.attribute(QStringLiteral("resource"))); } if (source.hasAttribute(QStringLiteral("file_hash"))) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:file_hash"), source.attribute(QStringLiteral("file_hash"))); } if (source.hasAttribute(QStringLiteral("file_size"))) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:file_size"), source.attribute(QStringLiteral("file_size"))); } if (source.hasAttribute(QStringLiteral("name"))) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:clipname"), source.attribute(QStringLiteral("name"))); } if (source.hasAttribute(QStringLiteral("zone_out"))) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:zone_out"), source.attribute(QStringLiteral("zone_out"))); } if (source.hasAttribute(QStringLiteral("zone_in"))) { EffectsList::setProperty(prod, QStringLiteral("kdenlive:zone_in"), source.attribute(QStringLiteral("zone_in"))); } if (source.hasAttribute(QStringLiteral("cutzones"))) { QString zoneData = source.attribute(QStringLiteral("cutzones")); const QStringList zoneList = zoneData.split(QLatin1Char(';')); int ct = 1; for (const QString &data : zoneList) { QString zoneName = data.section(QLatin1Char('-'), 2); if (zoneName.isEmpty()) { zoneName = i18n("Zone %1", ct++); } EffectsList::setProperty(prod, QStringLiteral("kdenlive:clipzone.") + zoneName, data.section(QLatin1Char('-'), 0, 0) + QLatin1Char(';') + data.section(QLatin1Char('-'), 1, 1)); } } } QStringList DocumentValidator::getInfoFromEffectName(const QString &oldName) { QStringList info; // Returns a list to convert old Kdenlive ladspa effects if (oldName == QLatin1String("pitch_shift")) { info << QStringLiteral("ladspa.1433"); info << QStringLiteral("pitch=0"); } else if (oldName == QLatin1String("vinyl")) { info << QStringLiteral("ladspa.1905"); info << QStringLiteral("year=0"); info << QStringLiteral("rpm=1"); info << QStringLiteral("warping=2"); info << QStringLiteral("crackle=3"); info << QStringLiteral("wear=4"); } else if (oldName == QLatin1String("room_reverb")) { info << QStringLiteral("ladspa.1216"); info << QStringLiteral("room=0"); info << QStringLiteral("delay=1"); info << QStringLiteral("damp=2"); } else if (oldName == QLatin1String("reverb")) { info << QStringLiteral("ladspa.1423"); info << QStringLiteral("room=0"); info << QStringLiteral("damp=1"); } else if (oldName == QLatin1String("rate_scale")) { info << QStringLiteral("ladspa.1417"); info << QStringLiteral("rate=0"); } else if (oldName == QLatin1String("pitch_scale")) { info << QStringLiteral("ladspa.1193"); info << QStringLiteral("coef=0"); } else if (oldName == QLatin1String("phaser")) { info << QStringLiteral("ladspa.1217"); info << QStringLiteral("rate=0"); info << QStringLiteral("depth=1"); info << QStringLiteral("feedback=2"); info << QStringLiteral("spread=3"); } else if (oldName == QLatin1String("limiter")) { info << QStringLiteral("ladspa.1913"); info << QStringLiteral("gain=0"); info << QStringLiteral("limit=1"); info << QStringLiteral("release=2"); } else if (oldName == QLatin1String("equalizer_15")) { info << QStringLiteral("ladspa.1197"); info << QStringLiteral("1=0"); info << QStringLiteral("2=1"); info << QStringLiteral("3=2"); info << QStringLiteral("4=3"); info << QStringLiteral("5=4"); info << QStringLiteral("6=5"); info << QStringLiteral("7=6"); info << QStringLiteral("8=7"); info << QStringLiteral("9=8"); info << QStringLiteral("10=9"); info << QStringLiteral("11=10"); info << QStringLiteral("12=11"); info << QStringLiteral("13=12"); info << QStringLiteral("14=13"); info << QStringLiteral("15=14"); } else if (oldName == QLatin1String("equalizer")) { info << QStringLiteral("ladspa.1901"); info << QStringLiteral("logain=0"); info << QStringLiteral("midgain=1"); info << QStringLiteral("higain=2"); } else if (oldName == QLatin1String("declipper")) { info << QStringLiteral("ladspa.1195"); } return info; } QString DocumentValidator::colorToString(const QColor &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha()); return ret; } bool DocumentValidator::isProject() const { return m_doc.documentElement().tagName() == QLatin1String("mlt"); } bool DocumentValidator::isModified() const { return m_modified; } bool DocumentValidator::checkMovit() { QString playlist = m_doc.toString(); if (!playlist.contains(QStringLiteral("movit."))) { // Project does not use Movit GLSL effects, we can load it return true; } if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("The project file uses some GPU effects. GPU acceleration is not currently enabled.\nDo you want to convert the " "project to a non-GPU version ?\nThis might result in data loss.")) != KMessageBox::Yes) { return false; } // Try to convert Movit filters to their non GPU equivalent QStringList convertedFilters; QStringList discardedFilters; int ix = MainWindow::videoEffects.hasEffect(QStringLiteral("frei0r.colgate"), QStringLiteral("frei0r.colgate")); bool hasWB = ix > -1; ix = MainWindow::videoEffects.hasEffect(QStringLiteral("frei0r.IIRblur"), QStringLiteral("frei0r.IIRblur")); bool hasBlur = ix > -1; QString compositeTrans; if (MainWindow::transitions.hasTransition(QStringLiteral("qtblend"))) { compositeTrans = QStringLiteral("qtblend"); } else if (MainWindow::transitions.hasTransition(QStringLiteral("frei0r.cairoblend"))) { compositeTrans = QStringLiteral("frei0r.cairoblend"); } // Parse all effects in document QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); int max = filters.count(); for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QString filterId = filt.attribute(QStringLiteral("id")); if (!filterId.startsWith(QLatin1String("movit."))) { continue; } if (filterId == QLatin1String("movit.white_balance") && hasWB) { // Convert to frei0r.colgate filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.colgate")); EffectsList::setProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.colgate")); EffectsList::setProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.colgate")); EffectsList::setProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.colgate")); EffectsList::renameProperty(filt, QStringLiteral("neutral_color"), QStringLiteral("Neutral Color")); QString value = EffectsList::property(filt, QStringLiteral("color_temperature")); value = factorizeGeomValue(value, 15000.0); EffectsList::setProperty(filt, QStringLiteral("color_temperature"), value); EffectsList::renameProperty(filt, QStringLiteral("color_temperature"), QStringLiteral("Color Temperature")); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.blur") && hasBlur) { // Convert to frei0r.IIRblur filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.IIRblur")); EffectsList::setProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.IIRblur")); EffectsList::setProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.IIRblur")); EffectsList::setProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.IIRblur")); EffectsList::renameProperty(filt, QStringLiteral("radius"), QStringLiteral("Amount")); QString value = EffectsList::property(filt, QStringLiteral("Amount")); value = factorizeGeomValue(value, 14.0); EffectsList::setProperty(filt, QStringLiteral("Amount"), value); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.mirror")) { // Convert to MLT's mirror filt.setAttribute(QStringLiteral("id"), QStringLiteral("mirror")); EffectsList::setProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("mirror")); EffectsList::setProperty(filt, QStringLiteral("tag"), QStringLiteral("mirror")); EffectsList::setProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("mirror")); EffectsList::setProperty(filt, QStringLiteral("mirror"), QStringLiteral("flip")); convertedFilters << filterId; continue; } if (filterId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << filterId; } } // Parse all transitions in document QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement t = transitions.at(i).toElement(); QString transId = EffectsList::property(t, QStringLiteral("mlt_service")); if (!transId.startsWith(QLatin1String("movit."))) { continue; } if (transId == QLatin1String("movit.overlay") && !compositeTrans.isEmpty()) { // Convert to frei0r.cairoblend EffectsList::setProperty(t, QStringLiteral("mlt_service"), compositeTrans); convertedFilters << transId; continue; } if (transId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << transId; } } convertedFilters.removeDuplicates(); discardedFilters.removeDuplicates(); if (discardedFilters.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were converted to non GPU versions:"), convertedFilters); } else { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were deleted from the project:"), discardedFilters); } m_modified = true; QString scene = m_doc.toString(); scene.replace(QLatin1String("movit."), QString()); m_doc.setContent(scene); return true; } QString DocumentValidator::factorizeGeomValue(const QString &value, double factor) { const QStringList vals = value.split(QLatin1Char(';')); QString result; QLocale locale; for (int i = 0; i < vals.count(); i++) { const QString &s = vals.at(i); QString key = s.section(QLatin1Char('='), 0, 0); QString val = s.section(QLatin1Char('='), 1, 1); double v = locale.toDouble(val) / factor; result.append(key + QLatin1Char('=') + locale.toString(v)); if (i + 1 < vals.count()) { result.append(QLatin1Char(';')); } } return result; } void DocumentValidator::checkOrphanedProducers() { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); QDomNodeList bin_producers = main.childNodes(); QStringList binProducers; for (int k = 0; k < bin_producers.count(); k++) { QDomElement mltprod = bin_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("entry")) { continue; } binProducers << mltprod.attribute(QStringLiteral("producer")); } QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList allProducers; for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) { continue; } allProducers << prod.attribute(QStringLiteral("id")); } QDomDocumentFragment frag = m_doc.createDocumentFragment(); QDomDocumentFragment trackProds = m_doc.createDocumentFragment(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) { continue; } QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (id.startsWith(QLatin1String("slowmotion")) || id == QLatin1String("black")) { continue; } if (!binProducers.contains(id)) { QString binId = EffectsList::property(prod, QStringLiteral("kdenlive:binid")); if (!binId.isEmpty() && binProducers.contains(binId)) { continue; } qCWarning(KDENLIVE_LOG) << " ///////// WARNING, FOUND UNKNOWN PRODUDER: " << id << " ----------------"; // This producer is unknown to Bin QString service = EffectsList::property(prod, QStringLiteral("mlt_service")); QString distinctiveTag(QStringLiteral("resource")); if (service == QLatin1String("kdenlivetitle")) { distinctiveTag = QStringLiteral("xmldata"); } QString orphanValue = EffectsList::property(prod, distinctiveTag); for (int j = 0; j < max; j++) { // Search for a similar producer QDomElement binProd = producers.at(j).toElement(); binId = binProd.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (service != QLatin1String("timewarp") && (binId.startsWith(QLatin1String("slowmotion")) || !binProducers.contains(binId))) { continue; } QString binService = EffectsList::property(binProd, QStringLiteral("mlt_service")); qCDebug(KDENLIVE_LOG) << " / /LKNG FOR: " << service << " / " << orphanValue << ", checking: " << binProd.attribute(QStringLiteral("id")); if (service != binService) { continue; } QString binValue = EffectsList::property(binProd, distinctiveTag); if (binValue == orphanValue) { // Found probable source producer, replace frag.appendChild(prod); i--; QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int k = 0; k < entries.count(); k++) { QDomElement entry = entries.at(k).toElement(); if (entry.attribute(QStringLiteral("producer")) == id) { QString entryId = binId; if (service.contains(QStringLiteral("avformat")) || service == QLatin1String("xml") || service == QLatin1String("consumer")) { // We must use track producer, find track for this entry QString trackPlaylist = entry.parentNode().toElement().attribute(QStringLiteral("id")); entryId.append(QLatin1Char('_') + trackPlaylist); } if (!allProducers.contains(entryId)) { // The track producer does not exist, create a clone for it QDomElement cloned = binProd.cloneNode(true).toElement(); cloned.setAttribute(QStringLiteral("id"), entryId); trackProds.appendChild(cloned); allProducers << entryId; } entry.setAttribute(QStringLiteral("producer"), entryId); m_modified = true; } } continue; } } } } if (!trackProds.isNull()) { QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); mlt.insertBefore(trackProds, firstProd); } } void DocumentValidator::fixTitleProducerLocale(QDomElement &producer) { QString data = EffectsList::property(producer, QStringLiteral("xmldata")); QDomDocument doc; doc.setContent(data); QDomNodeList nodes = doc.elementsByTagName(QStringLiteral("position")); bool fixed = false; for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("x")); QString y = pos.attribute(QStringLiteral("y")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("x"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("y"), y); fixed = true; } } nodes = doc.elementsByTagName(QStringLiteral("content")); for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("font-outline")); QString y = pos.attribute(QStringLiteral("textwidth")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("font-outline"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("textwidth"), y); fixed = true; } } if (fixed) { EffectsList::setProperty(producer, QStringLiteral("xmldata"), doc.toString()); } } diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index 97af0fe2c..622d43206 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -1,1640 +1,1615 @@ /*************************************************************************** * Copyright (C) 2007 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 "kdenlivedoc.h" #include "bin/bin.h" #include "bin/bincommands.h" +#include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "documentchecker.h" #include "documentvalidator.h" #include "docundostack.hpp" #include "effectslist/initeffects.h" +#include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/effectscontroller.h" -#include "mltcontroller/producerqueue.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/clipmanager.h" #include "project/projectcommands.h" #include "renderer.h" #include "titler/titlewidget.h" #include "transitions/transitionsrepository.hpp" #include "utils/KoIconUtils.h" #include #include #include #include -#include #include +#include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif const double DOCUMENTVERSION = 0.97; KdenliveDoc::KdenliveDoc(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, const QMap &metadata, const QPoint &tracks, bool *openBackup, MainWindow *parent) : QObject(parent) , m_autosave(nullptr) , m_url(url) , m_modified(false) , m_documentOpenStatus(CleanProject) , m_projectFolder(projectFolder) { m_commandStack = std::make_shared(undoGroup); m_guideModel.reset(new MarkerListModel(m_commandStack, this)); connect(m_guideModel.get(), &MarkerListModel::modelChanged, this, &KdenliveDoc::guidesChanged); m_clipManager = new ClipManager(this); connect(m_clipManager, SIGNAL(displayMessage(QString, int)), parent, SLOT(slotGotProgressInfo(QString, int))); connect(this, SIGNAL(updateCompositionMode(int)), parent, SLOT(slotUpdateCompositeAction(int))); bool success = false; connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified); connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack); - connect(pCore->producerQueue(), &ProducerQueue::switchProfile, this, &KdenliveDoc::switchProfile); // connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool))); // init default document properties m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('7'); m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1'); m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0'); m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("100"); m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int)KdenliveSettings::enableproxy()); m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams(); m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension(); m_documentProperties[QStringLiteral("previewparameters")] = KdenliveSettings::previewparams(); m_documentProperties[QStringLiteral("previewextension")] = KdenliveSettings::previewextension(); m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int)KdenliveSettings::generateproxy()); m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize()); m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int)KdenliveSettings::generateimageproxy()); m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize()); // Load properties QMapIterator i(properties); while (i.hasNext()) { i.next(); m_documentProperties[i.key()] = i.value(); } // Load metadata QMapIterator j(metadata); while (j.hasNext()) { j.next(); m_documentMetadata[j.key()] = j.value(); } if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { setlocale(LC_NUMERIC, ""); QLocale systemLocale = QLocale::system(); systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); // locale conversion might need to be redone initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); } *openBackup = false; if (url.isValid()) { QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // The file cannot be opened if (KMessageBox::warningContinueCancel(parent, i18n("Cannot open the project file,\nDo you want to open a backup file?"), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } // KMessageBox::error(parent, KIO::NetAccess::lastErrorString()); } else { qCDebug(KDENLIVE_LOG) << " // / processing file open"; QString errorMsg; int line; int col; QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars); success = m_document.setContent(&file, false, &errorMsg, &line, &col); file.close(); if (!success) { // It is corrupted int answer = KMessageBox::warningYesNoCancel( parent, i18n("Cannot open the project file, error is:\n%1 (line %2, col %3)\nDo you want to open a backup file?", errorMsg, line, col), i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover"))); if (answer == KMessageBox::Yes) { *openBackup = true; } else if (answer == KMessageBox::No) { // Try to recover broken file produced by Kdenlive 0.9.4 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { int correction = 0; QString playlist = QString::fromUtf8(file.readAll()); while (!success && correction < 2) { int errorPos = 0; line--; col = col - 2; for (int k = 0; k < line && errorPos < playlist.length(); ++k) { errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos); errorPos++; } errorPos += col; if (errorPos >= playlist.length()) { break; } playlist.remove(errorPos, 1); line = 0; col = 0; success = m_document.setContent(playlist, false, &errorMsg, &line, &col); correction++; } if (!success) { KMessageBox::sorry(parent, i18n("Cannot recover this project file")); } else { // Document was modified, ask for backup QDomElement mlt = m_document.documentElement(); mlt.setAttribute(QStringLiteral("modified"), 1); } } } } else { qCDebug(KDENLIVE_LOG) << " // / processing file open: validate"; parent->slotGotProgressInfo(i18n("Validating"), 100); qApp->processEvents(); DocumentValidator validator(m_document, url); success = validator.isProject(); if (!success) { // It is not a project file parent->slotGotProgressInfo(i18n("File %1 is not a Kdenlive project file", m_url.toLocalFile()), 100); if (KMessageBox::warningContinueCancel( parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.toLocalFile()), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } } else { /* * Validate the file against the current version (upgrade * and recover it if needed). It is NOT a passive operation */ // TODO: backup the document or alert the user? success = validator.validate(DOCUMENTVERSION); if (success && !KdenliveSettings::gpu_accel()) { success = validator.checkMovit(); } if (success) { // Let the validator handle error messages qCDebug(KDENLIVE_LOG) << " // / processing file validate ok"; parent->slotGotProgressInfo(i18n("Check missing clips"), 100); qApp->processEvents(); DocumentChecker d(m_url, m_document); success = !d.hasErrorInClips(); if (success) { loadDocumentProperties(); if (m_document.documentElement().hasAttribute(QStringLiteral("upgraded"))) { m_documentOpenStatus = UpgradedProject; pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"), ErrorMessage); } else if (m_document.documentElement().hasAttribute(QStringLiteral("modified")) || validator.isModified()) { m_documentOpenStatus = ModifiedProject; pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"), ErrorMessage); setModified(true); } } } } } } } // Something went wrong, or a new file was requested: create a new project if (!success) { m_url.clear(); pCore->setCurrentProfile(profileName); m_document = createEmptyDocument(tracks.x(), tracks.y()); updateProjectProfile(false); } if (!m_projectFolder.isEmpty()) { // Ask to create the project directory if it does not exist QDir folder(m_projectFolder); if (!folder.mkpath(QStringLiteral("."))) { // Project folder is not writable m_projectFolder = m_url.toString(QUrl::RemoveFilename | QUrl::RemoveScheme); folder.setPath(m_projectFolder); if (folder.exists()) { KMessageBox::sorry( parent, i18n("The project directory %1, could not be created.\nPlease make sure you have the required permissions.\nDefaulting to system folders", m_projectFolder)); } else { KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders")); } m_projectFolder.clear(); } } initCacheDirs(); updateProjectFolderPlacesEntry(); } KdenliveDoc::~KdenliveDoc() { if (m_url.isEmpty()) { // Document was never saved, delete cache folder QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (ok && !documentId.isEmpty()) { QDir baseCache = getCacheDir(CacheBase, &ok); if (baseCache.dirName() == documentId && baseCache.entryList(QDir::Files).isEmpty()) { baseCache.removeRecursively(); } } } // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN"; - //Clean up guide model + // Clean up guide model m_guideModel.reset(); delete m_clipManager; // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN done"; if (m_autosave) { if (!m_autosave->fileName().isEmpty()) { m_autosave->remove(); } delete m_autosave; } } const QByteArray KdenliveDoc::getProjectXml() { return m_document.toString().toUtf8(); } -void KdenliveDoc::loadThumbs() -{ - bool ok = false; - QDir thumbsFolder = getCacheDir(CacheThumbs, &ok); - if (ok) { - pCore->binController()->checkThumbnails(thumbsFolder); - } -} - QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks) { QList tracks; // Tracks are added «backwards», so we need to reverse the track numbering // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331 // Better default names for tracks: Audio 1 etc. instead of blank numbers tracks.reserve(audiotracks + videotracks); for (int i = 0; i < audiotracks; ++i) { TrackInfo audioTrack; audioTrack.type = AudioTrack; audioTrack.isMute = false; audioTrack.isBlind = true; audioTrack.isLocked = false; audioTrack.trackName = i18n("Audio %1", audiotracks - i); audioTrack.duration = 0; audioTrack.effectsList = EffectsList(true); tracks.append(audioTrack); } for (int i = 0; i < videotracks; ++i) { TrackInfo videoTrack; videoTrack.type = VideoTrack; videoTrack.isMute = false; videoTrack.isBlind = false; videoTrack.isLocked = false; videoTrack.trackName = i18n("Video %1", i + 1); videoTrack.duration = 0; videoTrack.effectsList = EffectsList(true); tracks.append(videoTrack); } return createEmptyDocument(tracks); } QDomDocument KdenliveDoc::createEmptyDocument(const QList &tracks) { // Creating new document QDomDocument doc; Mlt::Profile docProfile; Mlt::Consumer xmlConsumer(docProfile, "xml:kdenlive_playlist"); xmlConsumer.set("no_profile", 1); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); Mlt::Tractor tractor(docProfile); Mlt::Producer bk(docProfile, "color:black"); tractor.insert_track(bk, 0); for (int i = 0; i < tracks.count(); ++i) { Mlt::Tractor track(docProfile); track.set("kdenlive:track_name", tracks.at(i).trackName.toUtf8().constData()); track.set("kdenlive:trackheight", KdenliveSettings::trackheight()); if (tracks.at(i).type == AudioTrack) { track.set("kdenlive:audio_track", 1); } if (tracks.at(i).isLocked) { track.set("kdenlive:locked_track", 1); } if (tracks.at(i).isMute) { if (tracks.at(i).isBlind) { track.set("hide", 3); } else { track.set("hide", 2); } } else if (tracks.at(i).isBlind) { track.set("hide", 1); } Mlt::Playlist playlist1(docProfile); Mlt::Playlist playlist2(docProfile); track.insert_track(playlist1, 0); track.insert_track(playlist2, 1); tractor.insert_track(track, i + 1); } QScopedPointer field(tractor.field()); QString compositeService = TransitionsRepository::get()->getCompositingTransition(); - if (!compositeService.isEmpty()){ + if (!compositeService.isEmpty()) { for (int i = 0; i <= tracks.count(); i++) { if (i > 0) { Mlt::Transition tr(docProfile, "mix"); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("sum", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } if (i > 0 && tracks.at(i - 1).type == VideoTrack) { Mlt::Transition tr(docProfile, compositeService.toUtf8().constData()); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } } } Mlt::Producer prod(tractor.get_producer()); xmlConsumer.connect(prod); xmlConsumer.run(); QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")); doc.setContent(playlist); return doc; } bool KdenliveDoc::useProxy() const { return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0; } bool KdenliveDoc::autoGenerateProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt(); } bool KdenliveDoc::autoGenerateImageProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt(); } void KdenliveDoc::slotAutoSave() { // TODO: re-enable when qml timeline is ready return; if (m_autosave != nullptr) { if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) { // show error: could not open the autosave file qCDebug(KDENLIVE_LOG) << "ERROR; CANNOT CREATE AUTOSAVE FILE"; } // qCDebug(KDENLIVE_LOG) << "// AUTOSAVE FILE: " << m_autosave->fileName(); QDomDocument sceneList = xmlSceneList(pCore->monitorManager()->projectMonitor()->sceneList(m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile())); if (sceneList.isNull()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName())); return; } m_autosave->resize(0); m_autosave->write(sceneList.toString().toUtf8()); m_autosave->flush(); } } void KdenliveDoc::setZoom(int horizontal, int vertical) { m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal); m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical); } QPoint KdenliveDoc::zoom() const { return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt()); } void KdenliveDoc::setZone(int start, int end) { m_documentProperties[QStringLiteral("zonein")] = QString::number(start); m_documentProperties[QStringLiteral("zoneout")] = QString::number(end); } QPoint KdenliveDoc::zone() const { return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt()); } QDomDocument KdenliveDoc::xmlSceneList(const QString &scene) { QDomDocument sceneList; sceneList.setContent(scene, true); QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt")); if (mlt.isNull() || !mlt.hasChildNodes()) { // scenelist is corrupted return sceneList; } // Set playlist audio volume to 100% QDomElement tractor = mlt.firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist")); QDomElement mainPlaylist; for (int i = 0; i < pls.count(); ++i) { if (pls.at(i).toElement().attribute(QStringLiteral("id")) == pCore->binController()->binPlaylistId()) { mainPlaylist = pls.at(i).toElement(); break; } } // check if project contains custom effects to embed them in project file QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter")); int maxEffects = effects.count(); // qCDebug(KDENLIVE_LOG) << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++"; QMap effectIds; for (int i = 0; i < maxEffects; ++i) { QDomNode m = effects.at(i); QDomNodeList params = m.childNodes(); QString id; QString tag; for (int j = 0; j < params.count(); ++j) { QDomElement e = params.item(j).toElement(); if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) { id = e.firstChild().nodeValue(); } if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) { tag = e.firstChild().nodeValue(); } if (!id.isEmpty() && !tag.isEmpty()) { effectIds.insert(id, tag); } } } // TODO: find a way to process this before rendering MLT scenelist to xml QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds); if (!customeffects.documentElement().childNodes().isEmpty()) { EffectsList::setProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString()); } // addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true)); // TODO: move metadata to previous step in saving process QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata")); QMapIterator j(m_documentMetadata); while (j.hasNext()) { j.next(); docmetadata.setAttribute(j.key(), j.value()); } // addedXml.appendChild(docmetadata); return sceneList; } bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene) { QDomDocument sceneList = xmlSceneList(scene); if (sceneList.isNull()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path)); return false; } // Backup current version backupLastSavedVersion(path); if (m_documentOpenStatus != CleanProject) { // create visible backup file and warn user QString baseFile = path.section(QStringLiteral(".kdenlive"), 0, 0); int ct = 0; QString backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); while (QFile::exists(backupFile)) { ct++; backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); } QString message; if (m_documentOpenStatus == UpgradedProject) { message = i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you don't lose data, a backup copy called %1 " "was created.", backupFile); } else { message = i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile); } KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { KMessageBox::information(QApplication::activeWindow(), message); m_documentOpenStatus = CleanProject; } else { KMessageBox::information( - QApplication::activeWindow(), i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.", - backupFile)); + QApplication::activeWindow(), + i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.", + backupFile)); } } QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << path; KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); return false; } file.write(sceneList.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); file.close(); return false; } file.close(); cleanupBackupFiles(); QFileInfo info(file); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive.png")); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName)); return true; } ClipManager *KdenliveDoc::clipManager() { return m_clipManager; } QString KdenliveDoc::projectTempFolder() const { if (m_projectFolder.isEmpty()) { return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } return m_projectFolder; } QString KdenliveDoc::projectDataFolder() const { if (m_projectFolder.isEmpty()) { return KdenliveSettings::defaultprojectfolder(); } return m_projectFolder; } void KdenliveDoc::setProjectFolder(const QUrl &url) { if (url == QUrl::fromLocalFile(m_projectFolder)) { return; } setModified(true); QDir dir(url.toLocalFile()); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } dir.mkdir(QStringLiteral("titles")); /*if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the * new folder %2?", m_projectFolder, url.path())) == KMessageBox::Yes) moveProjectData(url);*/ m_projectFolder = url.toLocalFile(); updateProjectFolderPlacesEntry(); } void KdenliveDoc::moveProjectData(const QString & /*src*/, const QString &dest) { // Move proxies QList> list = pCore->binController()->getControllerList(); QList cacheUrls; for (int i = 0; i < list.count(); ++i) { const std::shared_ptr &clip = list.at(i); - if (clip->clipType() == Text) { + if (clip->clipType() == ClipType::Text) { // the image for title clip must be moved QUrl oldUrl = QUrl::fromLocalFile(clip->clipUrl()); if (!oldUrl.isEmpty()) { QUrl newUrl = QUrl::fromLocalFile(dest + QStringLiteral("/titles/") + oldUrl.fileName()); KIO::Job *job = KIO::copy(oldUrl, newUrl); if (job->exec()) { clip->setProducerProperty(QStringLiteral("resource"), newUrl.toLocalFile()); } } continue; } QString proxy = clip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (proxy.length() > 2 && QFile::exists(proxy)) { QUrl pUrl = QUrl::fromLocalFile(proxy); if (!cacheUrls.contains(pUrl)) { cacheUrls << pUrl; } } } if (!cacheUrls.isEmpty()) { QDir proxyDir(dest + QStringLiteral("/proxy/")); if (proxyDir.mkpath(QStringLiteral("."))) { KIO::CopyJob *job = KIO::move(cacheUrls, QUrl::fromLocalFile(proxyDir.absolutePath())); KJobWidgets::setWindow(job, QApplication::activeWindow()); if (static_cast(job->exec()) > 0) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Moving proxy clips failed: %1", job->errorText())); } } } } bool KdenliveDoc::profileChanged(const QString &profile) const { return pCore->getCurrentProfile() != ProfileRepository::get()->getProfile(profile); } Render *KdenliveDoc::renderer() { return nullptr; } std::shared_ptr KdenliveDoc::commandStack() { return m_commandStack; } int KdenliveDoc::getFramePos(const QString &duration) { return m_timecode.getFrameCount(duration); } QDomDocument KdenliveDoc::toXml() { return m_document; } Timecode KdenliveDoc::timecode() const { return m_timecode; } QDomNodeList KdenliveDoc::producersList() { return m_document.elementsByTagName(QStringLiteral("producer")); } double KdenliveDoc::projectDuration() const { return GenTime(pCore->monitorManager()->projectMonitor()->duration(), pCore->getCurrentFps()).ms() / 1000; } int KdenliveDoc::width() const { return pCore->getCurrentProfile()->width(); } int KdenliveDoc::height() const { return pCore->getCurrentProfile()->height(); } QUrl KdenliveDoc::url() const { return m_url; } void KdenliveDoc::setUrl(const QUrl &url) { m_url = url; } void KdenliveDoc::slotModified() { setModified(!m_commandStack->isClean()); } void KdenliveDoc::setModified(bool mod) { // fix mantis#3160: The document may have an empty URL if not saved yet, but should have a m_autosave in any case if ((m_autosave != nullptr) && mod && KdenliveSettings::crashrecovery()) { emit startAutoSave(); } if (mod == m_modified) { return; } m_modified = mod; emit docModified(m_modified); } bool KdenliveDoc::isModified() const { return m_modified; } const QString KdenliveDoc::description() const { if (!m_url.isValid()) { return i18n("Untitled") + QStringLiteral("[*] / ") + pCore->getCurrentProfile()->description(); } return m_url.fileName() + QStringLiteral(" [*]/ ") + pCore->getCurrentProfile()->description(); } QString KdenliveDoc::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const { QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (file.open(QIODevice::ReadOnly)) { if (QString::number(file.size()) == matchSize) { /* - * 1 MB = 1 second per 450 files (or faster) - * 10 MB = 9 seconds per 450 files (or faster) - */ + * 1 MB = 1 second per 450 files (or faster) + * 10 MB = 9 seconds per 450 files (or faster) + */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString::fromLatin1(fileHash.toHex()) == matchHash) { return file.fileName(); } qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << "size match but not hash"; } } ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } -void KdenliveDoc::deleteClip(const QString &clipId, ClipType type, const QString &url) -{ - pCore->binController()->removeBinClip(clipId); -} - +// TODO refac : delete std::shared_ptr KdenliveDoc::getBinClip(const QString &clipId) { return pCore->bin()->getBinClip(clipId); } QStringList KdenliveDoc::getBinFolderClipIds(const QString &folderId) const { return pCore->bin()->getBinFolderClipIds(folderId); } void KdenliveDoc::slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path) { + // TODO refac: this seem to be a duplicate of ClipCreationDialog::createTitleTemplateClip. See if we can merge QString titlesFolder = QDir::cleanPath(m_projectFolder + QStringLiteral("/titles/")); if (path.isEmpty()) { QPointer d = new QFileDialog(QApplication::activeWindow(), i18n("Enter Template Path"), titlesFolder); d->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle")); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { path = d->selectedUrls().first(); } delete d; } if (path.isEmpty()) { return; } // TODO: rewrite with new title system (just set resource) - m_clipManager->slotAddTextTemplateClip(i18n("Template title clip"), path, group, groupId); - emit selectLastAddedClip(QString::number(m_clipManager->lastClipId())); + QString id = ClipCreator::createTitleTemplate(path.toString(), QString(), i18n("Template title clip"), groupId, pCore->projectItemModel()); + emit selectLastAddedClip(id); } void KdenliveDoc::cacheImage(const QString &fileId, const QImage &img) const { bool ok = false; QDir dir = getCacheDir(CacheThumbs, &ok); if (ok) { img.save(dir.absoluteFilePath(fileId + QStringLiteral(".png"))); } } void KdenliveDoc::setDocumentProperty(const QString &name, const QString &value) { if (value.isEmpty()) { m_documentProperties.remove(name); return; } m_documentProperties[name] = value; } const QString KdenliveDoc::getDocumentProperty(const QString &name, const QString &defaultValue) const { return m_documentProperties.value(name, defaultValue); } QMap KdenliveDoc::getRenderProperties() const { QMap renderProperties; QMapIterator i(m_documentProperties); while (i.hasNext()) { i.next(); if (i.key().startsWith(QLatin1String("render"))) { if (i.key() == QLatin1String("renderurl")) { // Check that we have a full path QString value = i.value(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } renderProperties.insert(i.key(), value); } else { renderProperties.insert(i.key(), i.value()); } } } return renderProperties; } void KdenliveDoc::saveCustomEffects(const QDomNodeList &customeffects) { QDomElement e; QStringList importedEffects; int maxchild = customeffects.count(); for (int i = 0; i < maxchild; ++i) { e = customeffects.at(i).toElement(); const QString id = e.attribute(QStringLiteral("id")); const QString tag = e.attribute(QStringLiteral("tag")); if (!id.isEmpty()) { // Check if effect exists or save it if (MainWindow::customEffects.hasEffect(tag, id) == -1) { QDomDocument doc; doc.appendChild(doc.importNode(e, true)); QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects"); path += id + QStringLiteral(".xml"); if (!QFile::exists(path)) { importedEffects << id; QFile file(path); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } } } } } if (!importedEffects.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following effects were imported from the project:"), importedEffects); } if (!importedEffects.isEmpty()) { emit reloadEffects(); } } void KdenliveDoc::updateProjectFolderPlacesEntry() { /* * For similar and more code have a look at kfileplacesmodel.cpp and the included files: * http://websvn.kde.org/trunk/KDE/kdelibs/kfile/kfileplacesmodel.cpp?view=markup */ const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/user-places.xbel"); KBookmarkManager *bookmarkManager = KBookmarkManager::managerForExternalFile(file); if (!bookmarkManager) { return; } KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QString kdenliveName = QCoreApplication::applicationName(); QUrl documentLocation = QUrl::fromLocalFile(m_projectFolder); bool exists = false; while (!bookmark.isNull()) { // UDI not empty indicates a device QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (udi.isEmpty() && appName == kdenliveName && bookmark.text() == i18n("Project Folder")) { if (bookmark.url() != documentLocation) { bookmark.setUrl(documentLocation); bookmarkManager->emitChanged(root); } exists = true; break; } bookmark = root.next(bookmark); } // if entry does not exist yet (was not found), well, create it then if (!exists) { bookmark = root.addBookmark(i18n("Project Folder"), documentLocation, QStringLiteral("folder-favorites")); // Make this user selectable ? bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), kdenliveName); bookmarkManager->emitChanged(root); } } // static double KdenliveDoc::getDisplayRatio(const QString &path) { QFile file(path); QDomDocument doc; if (!file.open(QIODevice::ReadOnly)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; return 0; } if (!doc.setContent(&file)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; file.close(); return 0; } file.close(); QDomNodeList list = doc.elementsByTagName(QStringLiteral("profile")); if (list.isEmpty()) { return 0; } QDomElement profile = list.at(0).toElement(); double den = profile.attribute(QStringLiteral("display_aspect_den")).toDouble(); if (den > 0) { return profile.attribute(QStringLiteral("display_aspect_num")).toDouble() / den; } return 0; } void KdenliveDoc::backupLastSavedVersion(const QString &path) { // Ensure backup folder exists if (path.isEmpty()) { return; } QFile file(path); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); QFileInfo info(file); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive")); QString backupFile = backupFolder.absoluteFilePath(fileName); if (file.exists()) { // delete previous backup if it was done less than 60 seconds ago QFile::remove(backupFile); if (!QFile::copy(path, backupFile)) { KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", backupFile)); } } } void KdenliveDoc::cleanupBackupFiles() { QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString projectFile = url().fileName().section(QLatin1Char('.'), 0, -2); projectFile.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??.kdenlive")); QStringList filter; filter << projectFile; backupFolder.setNameFilters(filter); QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time); QDateTime d = QDateTime::currentDateTime(); QStringList hourList; QStringList dayList; QStringList weekList; QStringList oldList; for (int i = 0; i < resultList.count(); ++i) { if (d.secsTo(resultList.at(i).lastModified()) < 3600) { // files created in the last hour hourList.append(resultList.at(i).absoluteFilePath()); } else if (d.secsTo(resultList.at(i).lastModified()) < 43200) { // files created in the day dayList.append(resultList.at(i).absoluteFilePath()); } else if (d.daysTo(resultList.at(i).lastModified()) < 8) { // files created in the week weekList.append(resultList.at(i).absoluteFilePath()); } else { // older files oldList.append(resultList.at(i).absoluteFilePath()); } } if (hourList.count() > 20) { int step = hourList.count() / 10; for (int i = 0; i < hourList.count(); i += step) { // qCDebug(KDENLIVE_LOG)<<"REMOVE AT: "< 20) { int step = dayList.count() / 10; for (int i = 0; i < dayList.count(); i += step) { dayList.removeAt(i); --i; } } else { dayList.clear(); } if (weekList.count() > 20) { int step = weekList.count() / 10; for (int i = 0; i < weekList.count(); i += step) { weekList.removeAt(i); --i; } } else { weekList.clear(); } if (oldList.count() > 20) { int step = oldList.count() / 10; for (int i = 0; i < oldList.count(); i += step) { oldList.removeAt(i); --i; } } else { oldList.clear(); } QString f; while (hourList.count() > 0) { f = hourList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (dayList.count() > 0) { f = dayList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (weekList.count() > 0) { f = weekList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (oldList.count() > 0) { f = oldList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } } const QMap KdenliveDoc::metadata() const { return m_documentMetadata; } void KdenliveDoc::setMetadata(const QMap &meta) { setModified(true); m_documentMetadata = meta; } void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList> clipList, bool force, QUndoCommand *masterCommand) { if (clipList.isEmpty()) { clipList = pCore->bin()->selectedClips(); } bool hasParent = true; if (masterCommand == nullptr) { masterCommand = new QUndoCommand(); if (doProxy) { masterCommand->setText(i18np("Add proxy clip", "Add proxy clips", clipList.count())); } else { masterCommand->setText(i18np("Remove proxy clip", "Remove proxy clips", clipList.count())); } hasParent = false; } // Make sure the proxy folder exists bool ok = false; QDir dir = getCacheDir(CacheProxy, &ok); if (!ok) { // Error } QString extension = QLatin1Char('.') + getDocumentProperty(QStringLiteral("proxyextension")); QString params = getDocumentProperty(QStringLiteral("proxyparams")); if (params.contains(QStringLiteral("-s "))) { QString proxySize = params.section(QStringLiteral("-s "), 1).section(QStringLiteral("x"), 0, 0); extension.prepend(QStringLiteral("-") + proxySize); } // Prepare updated properties QMap newProps; QMap oldProps; if (!doProxy) { newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } // Parse clips for (int i = 0; i < clipList.count(); ++i) { std::shared_ptr item = clipList.at(i); ClipType t = item->clipType(); // Only allow proxy on some clip types - if ((t == Video || t == AV || t == Unknown || t == Image || t == Playlist || t == SlideShow) && item->isReady()) { + if ((t == ClipType::Video || t == ClipType::AV || t == ClipType::Unknown || t == ClipType::Image || t == ClipType::Playlist || + t == ClipType::SlideShow) && + item->isReady()) { if ((doProxy && !force && item->hasProxy()) || (!doProxy && !item->hasProxy() && pCore->binController()->hasClip(item->AbstractProjectItem::clipId()))) { continue; } - if (pCore->producerQueue()->isProcessing(item->AbstractProjectItem::clipId())) { - continue; - } if (doProxy) { newProps.clear(); - QString path = dir.absoluteFilePath(item->hash() + (t == Image ? QStringLiteral(".png") : extension)); + QString path = dir.absoluteFilePath(item->hash() + (t == ClipType::Image ? QStringLiteral(".png") : extension)); // insert required duration for proxy newProps.insert(QStringLiteral("proxy_out"), item->getProducerProperty(QStringLiteral("out"))); newProps.insert(QStringLiteral("kdenlive:proxy"), path); // We need to insert empty proxy so that undo will work // TODO: how to handle clip properties // oldProps = clip->currentProperties(newProps); oldProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } else { - if (t == SlideShow) { + if (t == ClipType::SlideShow) { // Revert to picture aspect ratio newProps.insert(QStringLiteral("aspect_ratio"), QStringLiteral("1")); } if (!pCore->binController()->hasClip(item->AbstractProjectItem::clipId())) { // Force clip reload newProps.insert(QStringLiteral("resource"), item->url()); } } new EditClipCommand(pCore->bin(), item->AbstractProjectItem::clipId(), oldProps, newProps, true, masterCommand); } else { // Cannot proxy this clip type pCore->bin()->doDisplayMessage(i18n("Clip type does not support proxies"), KMessageWidget::Information); } } if (!hasParent) { if (masterCommand->childCount() > 0) { m_commandStack->push(masterCommand); } else { delete masterCommand; } } } QMap KdenliveDoc::documentProperties() { m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION)); m_documentProperties.insert(QStringLiteral("kdenliveversion"), QStringLiteral(KDENLIVE_VERSION)); if (!m_projectFolder.isEmpty()) { m_documentProperties.insert(QStringLiteral("storagefolder"), m_projectFolder + QLatin1Char('/') + m_documentProperties.value(QStringLiteral("documentid"))); } m_documentProperties.insert(QStringLiteral("profile"), pCore->getCurrentProfile()->path()); m_documentProperties.insert(QStringLiteral("position"), QString::number(pCore->monitorManager()->projectMonitor()->position())); if (!m_documentProperties.contains(QStringLiteral("decimalPoint"))) { m_documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); } return m_documentProperties; } void KdenliveDoc::loadDocumentProperties() { QDomNodeList list = m_document.elementsByTagName(QStringLiteral("playlist")); QDomElement baseElement = m_document.documentElement(); m_documentRoot = baseElement.attribute(QStringLiteral("root")); if (!m_documentRoot.isEmpty()) { m_documentRoot = QDir::cleanPath(m_documentRoot) + QDir::separator(); } if (!list.isEmpty()) { QDomElement pl = list.at(0).toElement(); if (pl.isNull()) { return; } QDomNodeList props = pl.elementsByTagName(QStringLiteral("property")); QString name; QDomElement e; for (int i = 0; i < props.count(); i++) { e = props.at(i).toElement(); name = e.attribute(QStringLiteral("name")); if (name.startsWith(QLatin1String("kdenlive:docproperties."))) { name = name.section(QLatin1Char('.'), 1); if (name == QStringLiteral("storagefolder")) { // Make sure we have an absolute path QString value = e.firstChild().nodeValue(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } m_documentProperties.insert(name, value); } else if (name == QStringLiteral("guides")) { QString guides = e.firstChild().nodeValue(); if (!guides.isEmpty()) { - QMetaObject::invokeMethod(m_guideModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, guides), Q_ARG(bool, true), Q_ARG(bool, false)); + QMetaObject::invokeMethod(m_guideModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, guides), Q_ARG(bool, true), + Q_ARG(bool, false)); } } else { m_documentProperties.insert(name, e.firstChild().nodeValue()); } } else if (name.startsWith(QLatin1String("kdenlive:docmetadata."))) { name = name.section(QLatin1Char('.'), 1); m_documentMetadata.insert(name, e.firstChild().nodeValue()); } } } QString path = m_documentProperties.value(QStringLiteral("storagefolder")); if (!path.isEmpty()) { QDir dir(path); dir.cdUp(); m_projectFolder = dir.absolutePath(); } QString profile = m_documentProperties.value(QStringLiteral("profile")); bool profileFound = pCore->setCurrentProfile(profile); if (!profileFound) { // try to find matching profile from MLT profile properties list = m_document.elementsByTagName(QStringLiteral("profile")); if (!list.isEmpty()) { std::unique_ptr xmlProfile(new ProfileParam(list.at(0).toElement())); profileFound = pCore->setCurrentProfile(ProfileRepository::get()->findMatchingProfile(xmlProfile.get())); } } if (!profileFound) { qDebug() << "ERROR, no matching profile found"; } updateProjectProfile(false); } void KdenliveDoc::updateProjectProfile(bool reloadProducers) { pCore->bin()->abortAudioThumbs(); - pCore->producerQueue()->abortOperations(); + pCore->jobManager()->slotCancelJobs(); double fps = pCore->getCurrentFps(); double fpsChanged = m_timecode.fps() / fps; m_timecode.setFormat(fps); pCore->monitorManager()->resetProfiles(m_timecode); if (!reloadProducers) { return; } emit updateFps(fpsChanged); if (qAbs(fpsChanged - 1.0) > 1e-6) { pCore->bin()->reloadAllProducers(); } } void KdenliveDoc::resetProfile() { updateProjectProfile(true); emit docModified(true); } void KdenliveDoc::slotSwitchProfile(const QString &profile_path) { pCore->setCurrentProfile(profile_path); updateProjectProfile(true); emit docModified(true); } void KdenliveDoc::switchProfile(std::unique_ptr &profile, const QString &id, const QDomElement &xml) { // Request profile update QString matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); if (matchingProfile.isEmpty() && (profile->width() % 8 != 0)) { // Make sure profile width is a multiple of 8, required by some parts of mlt profile->adjustWidth(); matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); } if (!matchingProfile.isEmpty()) { // We found a known matching profile, switch and inform user profile->m_path = matchingProfile; profile->m_description = ProfileRepository::get()->getProfile(matchingProfile)->description(); if (KdenliveSettings::default_profile().isEmpty()) { // Default project format not yet confirmed, propose QString currentProfileDesc = pCore->getCurrentProfile()->description(); KMessageBox::ButtonCode answer = KMessageBox::questionYesNoCancel( QApplication::activeWindow(), i18n("Your default project profile is %1, but your clip's profile is %2.\nDo you want to change default profile for future projects ?", currentProfileDesc, profile->description()), i18n("Change default project profile"), KGuiItem(i18n("Change default to %1", profile->description())), KGuiItem(i18n("Keep current default %1", currentProfileDesc)), KGuiItem(i18n("Ask me later"))); switch (answer) { case KMessageBox::Yes: KdenliveSettings::setDefault_profile(profile->path()); pCore->setCurrentProfile(profile->path()); updateProjectProfile(true); emit docModified(true); - pCore->producerQueue()->getFileProperties(xml, id, 150, true); return; break; case KMessageBox::No: return; break; default: break; } } // Build actions for the info message (switch / cancel) QList list; QAction *ac = new QAction(KoIconUtils::themedIcon(QStringLiteral("dialog-ok")), i18n("Switch"), this); - connect(ac, &QAction::triggered, [this, &profile]() { - this->slotSwitchProfile(profile->path()); - }); + connect(ac, &QAction::triggered, [this, &profile]() { this->slotSwitchProfile(profile->path()); }); QAction *ac2 = new QAction(KoIconUtils::themedIcon(QStringLiteral("dialog-cancel")), i18n("Cancel"), this); list << ac << ac2; pCore->bin()->doDisplayMessage(i18n("Switch to clip profile %1?", profile->descriptiveString()), KMessageWidget::Information, list); } else { // No known profile, ask user if he wants to use clip profile anyway // Check profile fps so that we don't end up with an fps = 30.003 which would mess things up QString adjustMessage; double fps = (double)profile->frame_rate_num() / profile->frame_rate_den(); double fps_int; double fps_frac = std::modf(fps, &fps_int); if (fps_frac < 0.4) { profile->m_frame_rate_num = (int)fps_int; profile->m_frame_rate_den = 1; } else { // Check for 23.98, 29.97, 59.94 if (qAbs(fps_int - 23.0) < 1e-5) { if (qAbs(fps - 23.98) < 0.01) { profile->m_frame_rate_num = 24000; profile->m_frame_rate_den = 1001; } } else if (qAbs(fps_int - 29.0) < 1e-5) { if (qAbs(fps - 29.97) < 0.01) { profile->m_frame_rate_num = 30000; profile->m_frame_rate_den = 1001; } } else if (qAbs(fps_int - 59.0) < 1e-5) { if (qAbs(fps - 59.94) < 0.01) { profile->m_frame_rate_num = 60000; profile->m_frame_rate_den = 1001; } } else { // Unknown profile fps, warn user adjustMessage = i18n("\nWarning: unknown non integer fps, might cause incorrect duration display."); } } if (qAbs((double)profile->m_frame_rate_num / profile->m_frame_rate_den - fps) < 1e-4) { adjustMessage = i18n("\nProfile fps adjusted from original %1", QString::number(fps, 'f', 4)); } if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("No profile found for your clip.\nCreate and switch to new profile (%1x%2, %3fps)?%4", profile->m_width, profile->m_height, QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2), adjustMessage)) == KMessageBox::Continue) { profile->m_description = QStringLiteral("%1x%2 %3fps") - .arg(profile->m_width) - .arg(profile->m_height) - .arg(QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2)); + .arg(profile->m_width) + .arg(profile->m_height) + .arg(QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2)); ProfileRepository::get()->saveProfile(profile.get()); pCore->setCurrentProfile(profile->m_path); updateProjectProfile(true); emit docModified(true); - pCore->producerQueue()->getFileProperties(xml, id, 150, true); } } } -void KdenliveDoc::forceProcessing(const QString &id) -{ - pCore->producerQueue()->forceProcessing(id); -} - -void KdenliveDoc::getFileProperties(const QDomElement &xml, const QString &clipId, int imageHeight, bool replaceProducer) -{ - pCore->producerQueue()->getFileProperties(xml, clipId, imageHeight, replaceProducer); -} - void KdenliveDoc::doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut) { pCore->window()->actionCollection()->addAction(name, a); a->setShortcut(shortcut); pCore->window()->actionCollection()->setDefaultShortcut(a, a->shortcut()); } QAction *KdenliveDoc::getAction(const QString &name) { return pCore->window()->actionCollection()->action(name); } void KdenliveDoc::previewProgress(int p) { pCore->window()->setPreviewProgress(p); } void KdenliveDoc::displayMessage(const QString &text, MessageType type, int timeOut) { pCore->window()->displayMessage(text, type, timeOut); } void KdenliveDoc::selectPreviewProfile() { // Read preview profiles and find the best match if (!KdenliveSettings::previewparams().isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), KdenliveSettings::previewparams()); setDocumentProperty(QStringLiteral("previewextension"), KdenliveSettings::previewextension()); return; } KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap values = group.entryMap(); QMapIterator i(values); QStringList matchingProfiles; QStringList fallBackProfiles; QSize pSize = pCore->getCurrentFrameDisplaySize(); QString profileSize = QStringLiteral("%1x%2").arg(pSize.width()).arg(pSize.height()); while (i.hasNext()) { i.next(); // Check for frame rate QString params = i.value(); QStringList data = i.value().split(QLatin1Char(' ')); // Check for size mismatch if (params.contains(QStringLiteral("s="))) { QString paramSize = params.section(QStringLiteral("s="), 1).section(QLatin1Char(' '), 0, 0); if (paramSize != profileSize) { continue; } } bool rateFound = false; for (const QString &arg : data) { if (arg.startsWith(QStringLiteral("r="))) { rateFound = true; double fps = arg.section(QLatin1Char('='), 1).toDouble(); if (fps > 0) { if (qAbs((int)(pCore->getCurrentFps() * 100) - (fps * 100)) <= 1) { matchingProfiles << i.value(); break; } } } } if (!rateFound) { // Profile without fps, can be used as fallBack fallBackProfiles << i.value(); } } QString bestMatch; if (!matchingProfiles.isEmpty()) { bestMatch = matchingProfiles.first(); } else if (!fallBackProfiles.isEmpty()) { bestMatch = fallBackProfiles.first(); } if (!bestMatch.isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); } else { setDocumentProperty(QStringLiteral("previewparameters"), QString()); setDocumentProperty(QStringLiteral("previewextension"), QString()); } } void KdenliveDoc::checkPreviewStack() { // A command was pushed in the middle of the stack, remove all cached data from last undos emit removeInvalidUndo(m_commandStack->count()); } void KdenliveDoc::saveMltPlaylist(const QString &fileName) { // TODO REFAC // m_render->preparePreviewRendering(fileName); } void KdenliveDoc::initCacheDirs() { bool ok = false; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } else { kdenliveCacheDir = m_projectFolder; } if (!ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { return; } QString basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; QDir dir(basePath); dir.mkpath(QStringLiteral(".")); dir.mkdir(QStringLiteral("preview")); dir.mkdir(QStringLiteral("audiothumbs")); dir.mkdir(QStringLiteral("videothumbs")); QDir cacheDir(kdenliveCacheDir); cacheDir.mkdir(QStringLiteral("proxy")); } QDir KdenliveDoc::getCacheDir(CacheType type, bool *ok) const { QString basePath; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); if (!*ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { *ok = false; return QDir(kdenliveCacheDir); } } else { // Use specified folder to store all files kdenliveCacheDir = m_projectFolder; } basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; switch (type) { case SystemCacheRoot: return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); case CacheRoot: basePath = kdenliveCacheDir; break; case CachePreview: basePath.append(QStringLiteral("/preview")); break; case CacheProxy: basePath = kdenliveCacheDir; basePath.append(QStringLiteral("/proxy")); break; case CacheAudio: basePath.append(QStringLiteral("/audiothumbs")); break; case CacheThumbs: basePath.append(QStringLiteral("/videothumbs")); break; default: break; } QDir dir(basePath); if (!dir.exists()) { *ok = false; } return dir; } QStringList KdenliveDoc::getProxyHashList() { return pCore->bin()->getProxyHashList(); } std::shared_ptr KdenliveDoc::getGuideModel() const { return m_guideModel; } void KdenliveDoc::guidesChanged() { m_documentProperties[QStringLiteral("guides")] = m_guideModel->toJson(); } void KdenliveDoc::groupsChanged(const QString &groups) { m_documentProperties[QStringLiteral("groups")] = groups; } const QString KdenliveDoc::documentRoot() const { return m_documentRoot; } diff --git a/src/doc/kdenlivedoc.h b/src/doc/kdenlivedoc.h index 0c0633831..b37752a9f 100644 --- a/src/doc/kdenlivedoc.h +++ b/src/doc/kdenlivedoc.h @@ -1,252 +1,246 @@ /*************************************************************************** * Copyright (C) 2007 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 * ***************************************************************************/ /*! \class KdenliveDoc \brief Represents a kdenlive project file Instances of KdeliveDoc classes are created by void MainWindow::newFile(bool showProjectSettings, bool force) */ #ifndef KDENLIVEDOC_H #define KDENLIVEDOC_H #include #include #include #include #include #include #include #include "definitions.h" #include "gentime.h" #include "mltcontroller/effectscontroller.h" #include "timecode.h" class ClipManager; class MainWindow; class TrackInfo; class ProjectClip; class ClipController; class MarkerListModel; class Render; class ProfileParam; class QTextEdit; class QUndoGroup; class QUndoCommand; class DocUndoStack; namespace Mlt { class Profile; } class KdenliveDoc : public QObject { Q_OBJECT public: KdenliveDoc(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, const QMap &metadata, const QPoint &tracks, bool *openBackup, MainWindow *parent = nullptr); ~KdenliveDoc(); + friend class LoadJob; /** @brief Get current document's producer. */ const QByteArray getProjectXml(); QDomNodeList producersList(); double fps() const; int width() const; int height() const; QUrl url() const; KAutoSaveFile *m_autosave; Timecode timecode() const; QDomDocument toXml(); std::shared_ptr commandStack(); ClipManager *clipManager(); - void deleteClip(const QString &clipId, ClipType type, const QString &url); int getFramePos(const QString &duration); /** @brief Get a bin's clip from its id. */ std::shared_ptr getBinClip(const QString &clipId); /** @brief Get a list of all clip ids that are inside a folder. */ QStringList getBinFolderClipIds(const QString &folderId) const; const QString description() const; void setUrl(const QUrl &url); /** @brief Defines whether the document needs to be saved. */ bool isModified() const; /** @brief Returns the project folder, used to store project temporary files. */ QString projectTempFolder() const; /** @brief Returns the folder used to store project data files (titles, etc). */ QString projectDataFolder() const; void setZoom(int horizontal, int vertical); QPoint zoom() const; double dar() const; double projectDuration() const; /** @brief Returns the project file xml. */ QDomDocument xmlSceneList(const QString &scene); /** @brief Saves the project file xml to a file. */ bool saveSceneList(const QString &path, const QString &scene); /** @brief Saves only the MLT xml to a file for preview rendering. */ void saveMltPlaylist(const QString &fileName); void cacheImage(const QString &fileId, const QImage &img) const; void setProjectFolder(const QUrl &url); void setZone(int start, int end); QPoint zone() const; void setDocumentProperty(const QString &name, const QString &value); const QString getDocumentProperty(const QString &name, const QString &defaultValue = QString()) const; /** @brief Gets the list of renderer properties saved into the document. */ QMap getRenderProperties() const; /** @brief Read the display ratio from an xml project file. */ static double getDisplayRatio(const QString &path); /** @brief Backup the project file */ void backupLastSavedVersion(const QString &path); /** @brief Returns the document metadata (author, copyright, ...) */ const QMap metadata() const; /** @brief Set the document metadata (author, copyright, ...) */ void setMetadata(const QMap &meta); /** @brief Get all document properties that need to be saved */ QMap documentProperties(); bool useProxy() const; bool autoGenerateProxy(int width) const; bool autoGenerateImageProxy(int width) const; /** @brief Saves effects embedded in project file. */ void saveCustomEffects(const QDomNodeList &customeffects); void resetProfile(); - /** @brief Force processing of clip id in producer queue. */ - void forceProcessing(const QString &id); - void getFileProperties(const QDomElement &xml, const QString &clipId, int imageHeight, bool replaceProducer = true); /** @brief Returns true if the profile file has changed. */ bool profileChanged(const QString &profile) const; /** @brief Get an action from main actioncollection. */ QAction *getAction(const QString &name); /** @brief Add an action to main actioncollection. */ void doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut); void invalidatePreviews(QList chunks); void previewProgress(int p); /** @brief Select most appropriate rendering profile for timeline preview based on fps / size. */ void selectPreviewProfile(); void displayMessage(const QString &text, MessageType type = DefaultMessage, int timeOut = 0); /** @brief Get a cache directory for this project. */ QDir getCacheDir(CacheType type, bool *ok) const; /** @brief Create standard cache dirs for the project */ void initCacheDirs(); /** @brief Get a list of all proxy hash used in this project */ QStringList getProxyHashList(); /** @brief Move project data files to new url */ void moveProjectData(const QString &src, const QString &dest); /** @brief Returns a pointer to the guide model */ std::shared_ptr getGuideModel() const; - /** @brief Load bin thumbnails after document opening */ - void loadThumbs(); - // TODO REFAC: delete */ Render *renderer(); /** @brief Returns MLT's root (base path) */ const QString documentRoot() const; private: QUrl m_url; QDomDocument m_document; /** @brief MLT's root (base path) that is stripped from urls in saved xml */ QString m_documentRoot; Timecode m_timecode; std::shared_ptr m_commandStack; ClipManager *m_clipManager; QString m_searchFolder; /** @brief Tells whether the current document has been changed after being saved. */ bool m_modified; /** @brief Tells whether the current document was modified by Kdenlive on opening, and a backup should be created on save. */ enum DOCSTATUS {CleanProject, ModifiedProject, UpgradedProject}; DOCSTATUS m_documentOpenStatus; /** @brief The project folder, used to store project files (titles, effects...). */ QString m_projectFolder; QList m_undoChunks; QMap m_documentProperties; QMap m_documentMetadata; std::shared_ptr m_guideModel; QString searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const; /** @brief Creates a new project. */ QDomDocument createEmptyDocument(int videotracks, int audiotracks); QDomDocument createEmptyDocument(const QList &tracks); /** @brief Updates the project folder location entry in the kdenlive file dialogs to point to the current project folder. */ void updateProjectFolderPlacesEntry(); /** @brief Only keep some backup files, delete some */ void cleanupBackupFiles(); /** @brief Load document properties from the xml file */ void loadDocumentProperties(); /** @brief update document properties to reflect a change in the current profile */ void updateProjectProfile(bool reloadProducers = false); public slots: void slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path); /** @brief Sets the document as modified or up to date. * @description If crash recovery is turned on, a timer calls KdenliveDoc::slotAutoSave() \n * Emits docModified conected to MainWindow::slotUpdateDocumentState \n * @param mod (optional) true if the document has to be saved */ void setModified(bool mod = true); void slotProxyCurrentItem(bool doProxy, QList> clipList = QList>(), bool force = false, QUndoCommand *masterCommand = nullptr); /** @brief Saves the current project at the autosave location. * @description The autosave files are in ~/.kde/data/stalefiles/kdenlive/ */ void slotAutoSave(); /** @brief Groups were changed, save to MLT. */ void groupsChanged(const QString &groups); private slots: void slotModified(); void switchProfile(std::unique_ptr &profile, const QString &id, const QDomElement &xml); void slotSwitchProfile(const QString &profile_path); /** @brief Check if we did a new action invalidating more recent undo items. */ void checkPreviewStack(); /** @brief Guides were changed, save to MLT. */ void guidesChanged(); signals: void resetProjectList(); /** @brief Informs that the document status has been changed. * * If the document has been modified, it's called with true as an argument. */ void docModified(bool); void selectLastAddedClip(const QString &); /** @brief When creating a backup file, also save a thumbnail of current timeline */ void saveTimelinePreview(const QString &path); /** @brief Trigger the autosave timer start */ void startAutoSave(); /** @brief Current doc created effects, reload list */ void reloadEffects(); /** @brief Fps was changed, update timeline (changed = 1 means no change) */ void updateFps(double changed); /** @brief If a command is pushed when we are in the middle of undo stack, invalidate further undo history */ void removeInvalidUndo(int ix); /** @brief Update compositing info */ void updateCompositionMode(int); }; #endif diff --git a/src/jobs/CMakeLists.txt b/src/jobs/CMakeLists.txt new file mode 100644 index 000000000..d79d60378 --- /dev/null +++ b/src/jobs/CMakeLists.txt @@ -0,0 +1,14 @@ +set(kdenlive_SRCS + ${kdenlive_SRCS} + jobs/abstractclipjob.cpp + jobs/audiothumbjob.cpp + jobs/jobmanager.cpp + jobs/loadjob.cpp + jobs/meltjob.cpp + jobs/scenesplitjob.cpp + jobs/stabilizejob.cpp + jobs/thumbjob.cpp +# jobs/cutclipjob.cpp +# jobs/filterjob.cpp +# jobs/proxyclipjob.cpp + PARENT_SCOPE) diff --git a/src/project/jobs/abstractclipjob.cpp b/src/jobs/abstractclipjob.cpp similarity index 61% rename from src/project/jobs/abstractclipjob.cpp rename to src/jobs/abstractclipjob.cpp index c93add295..56f359c6c 100644 --- a/src/project/jobs/abstractclipjob.cpp +++ b/src/jobs/abstractclipjob.cpp @@ -1,102 +1,58 @@ /*************************************************************************** * * * 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 "abstractclipjob.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" -AbstractClipJob::AbstractClipJob(JOBTYPE type, ClipType cType, const QString &id, QObject *parent) +AbstractClipJob::AbstractClipJob(JOBTYPE type, const QString &id, QObject *parent) : QObject(parent) - , clipType(cType) - , jobType(type) - , replaceClip(false) - , m_jobStatus(NoJob) + , m_jobType(type) , m_clipId(id) - , m_addClipToProject(-100) - , m_jobProcess(nullptr) { } -AbstractClipJob::~AbstractClipJob() -{ -} - -int AbstractClipJob::addClipToProject() const -{ - return m_addClipToProject; -} - -void AbstractClipJob::setAddClipToProject(int add) -{ - m_addClipToProject = add; -} - -void AbstractClipJob::setStatus(ClipJobStatus status) -{ - m_jobStatus = status; -} - -ClipJobStatus AbstractClipJob::status() -{ - return m_jobStatus; -} +AbstractClipJob::~AbstractClipJob() {} const QString AbstractClipJob::clipId() const { return m_clipId; } -const QString AbstractClipJob::errorMessage() const +const QString AbstractClipJob::getErrorMessage() const { return m_errorMessage; } -const QString AbstractClipJob::logDetails() const +const QString AbstractClipJob::getLogDetails() const { return m_logDetails; } -void AbstractClipJob::startJob() -{ -} - -const QString AbstractClipJob::destination() const -{ - return QString(); -} - -stringMap AbstractClipJob::cancelProperties() -{ - return QMap(); -} - -void AbstractClipJob::processLogInfo() -{ -} - -const QString AbstractClipJob::statusMessage() +// static +bool AbstractClipJob::execute(std::shared_ptr job) { - return QString(); + return job->startJob(); } -bool AbstractClipJob::isExclusive() +AbstractClipJob::JOBTYPE AbstractClipJob::jobType() const { - return true; + return m_jobType; } diff --git a/src/project/jobs/abstractclipjob.h b/src/jobs/abstractclipjob.h similarity index 57% rename from src/project/jobs/abstractclipjob.h rename to src/jobs/abstractclipjob.h index 0ca790657..57bad75bf 100644 --- a/src/project/jobs/abstractclipjob.h +++ b/src/jobs/abstractclipjob.h @@ -1,75 +1,97 @@ /*************************************************************************** * * * 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 "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, MLTJOB = 3, TRANSCODEJOB = 4, FILTERCLIPJOB = 5, THUMBJOB = 5, ANALYSECLIPJOB = 6 }; - AbstractClipJob(JOBTYPE type, ClipType cType, const QString &id, QObject *parent = nullptr); + enum JOBTYPE { + NOJOBTYPE = 0, + PROXYJOB = 1, + CUTJOB = 2, + STABILIZEJOB = 3, + TRANSCODEJOB = 4, + FILTERCLIPJOB = 5, + THUMBJOB = 5, + ANALYSECLIPJOB = 6, + LOADJOB = 7, + AUDIOTHUMBJOB = 8 + }; + AbstractClipJob(JOBTYPE type, const QString &id, QObject *parent = nullptr); virtual ~AbstractClipJob(); - ClipType clipType; - JOBTYPE jobType; - QString description; - bool replaceClip; + + template static std::shared_ptr make(const QString &binId, Args &&... args) + { + auto m = std::make_shared(binId, std::forward(args)...); + return m; + } + const QString clipId() const; - const QString errorMessage() const; - const QString logDetails() const; - ClipJobStatus status(); - virtual void setStatus(ClipJobStatus status); - virtual const QString destination() const; - virtual void startJob(); - virtual stringMap cancelProperties(); - virtual void processLogInfo(); - virtual const QString statusMessage(); - /** @brief Returns true if only one instance of this job can be run on a clip. */ - virtual bool isExclusive(); - int addClipToProject() const; - void setAddClipToProject(int add); + 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; + + // brief run a given job + static bool execute(std::shared_ptr job); + + /* @brief return the type of this job */ + JOBTYPE jobType() const; protected: - ClipJobStatus m_jobStatus; QString m_clipId; QString m_errorMessage; QString m_logDetails; int m_addClipToProject; - QProcess *m_jobProcess; + JOBTYPE m_jobType; + + bool m_resultConsumed{false}; signals: - void jobProgress(const QString &, int, int); - void cancelRunningJob(const QString &, const QMap &); + // 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 new file mode 100644 index 000000000..7a180a0ed --- /dev/null +++ b/src/jobs/audiothumbjob.cpp @@ -0,0 +1,338 @@ +/*************************************************************************** + * 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) +{ +} + +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 + args << QStringLiteral("-filter_complex:a"); + 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=stereo") + .arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) + .arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates="); + 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.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 << channelsData[k] * factor; + } + int p = 80 + (i * 20 / m_lengthInFrames); + if (p != progress) { + emit jobProgress(p); + progress = p; + } + } + m_done = true; + return true; + } + 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"))) { + long ms = data.section(QLatin1Char('='), 1).toLong(); + emit jobProgress(ms / m_binClip->duration().ms() * 80.); + } else { + m_logDetails += data + QStringLiteral("\n"); + } + } + QString err = m_ffmpegProcess.readAllStandardError(); + m_errorMessage += err; +} + +bool AudioThumbJob::startJob() +{ + qDebug() << "################### audiothumbjob START"; + if (m_done) { + return true; + } + m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); + if (m_binClip->audioChannels() == 0) { + // nothing to do + m_done = 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 = computeWithFFMPEG(); + 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) +{ + 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/bin/clipcreator.hpp b/src/jobs/audiothumbjob.hpp similarity index 53% copy from src/bin/clipcreator.hpp copy to src/jobs/audiothumbjob.hpp index 0693a797a..1e33ac6db 100644 --- a/src/bin/clipcreator.hpp +++ b/src/jobs/audiothumbjob.hpp @@ -1,45 +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 . * ***************************************************************************/ -#ifndef CLIPCREATOR_H -#define CLIPCREATOR_H +#pragma once + +#include "abstractclipjob.h" -#include #include -/** @brief This namespace provides convenienc function to create clips based on various parameters +/* @brief This class represents the job that corresponds to computing the audio thumb of a clip (waveform) */ -class ProjectItemModel; -namespace ClipCreator +class ProjectClip; +namespace Mlt { +class Producer; +} + +class AudioThumbJob : public AbstractClipJob { - /* @brief Create and inserts a color clip - @param color : a string of the form "0xff0000ff" (solid red in RGBA) - @param duration : duration expressed in number of frames - @param name: name of the clip - @param parentFolder: the binId of the containing folder - @param model: a shared pointer to the bin item model - @return the binId of the created clip + 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) */ - QString createColorClip(const QString& color, int duration, const QString& name, const QString& parentFolder, std::shared_ptr model); -} + 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; + +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; -#endif + 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/createclipjob.cpp b/src/jobs/createclipjob.cpp new file mode 100644 index 000000000..48ced3857 --- /dev/null +++ b/src/jobs/createclipjob.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + * 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 "klocalizedstring.h" +#include "thumbjob.hpp" + +#include + +ThumbJob::ThumbJob(const QString &binId) + : AbstractClipJob(THUMBJOB, binId) +{ +} + +const QString ThumbJob::getDescription() const +{ + return i18n("Extracting thumb from clip %1", m_clipId); +} + +void ThumbJob::startJob() +{ + // Special case, we just want the thumbnail for existing producer + std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(info.clipId); + Mlt::Producer *prod = new Mlt::Producer(*binClip->originalProducer()); + if ((prod == nullptr) || !prod->is_valid()) { + continue; + } + + int frameNumber = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:thumbnailFrame"), QStringLiteral("-1")).toInt(); + if (frameNumber > 0) { + prod->seek(frameNumber); + } + QScopedPointer frame = prod->get_frame(); + frame->set("deinterlace_method", "onefield"); + frame->set("top_field_first", -1); + if ((frame != nullptr) && frame->is_valid()) { + int fullWidth = info.imageHeight * pCore->getCurrentDar() + 0.5; + QImage img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); + emit replyGetImage(info.clipId, img); + } + delete frame; + delete prod; + if (info.xml.hasAttribute(QStringLiteral("refreshOnly"))) { + // inform timeline about change + emit refreshTimelineProducer(info.clipId); + } +} +/** @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 */ +void commitResult(Fun &undo, Fun &redo) override; +} +; + +#endif diff --git a/src/bin/clipcreator.hpp b/src/jobs/createclipjob.hpp similarity index 66% copy from src/bin/clipcreator.hpp copy to src/jobs/createclipjob.hpp index 0693a797a..337e5b59c 100644 --- a/src/bin/clipcreator.hpp +++ b/src/jobs/createclipjob.hpp @@ -1,45 +1,49 @@ /*************************************************************************** * 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 CLIPCREATOR_H -#define CLIPCREATOR_H +#pragma once -#include -#include +#include "abstractclipjob.h" -/** @brief This namespace provides convenienc function to create clips based on various parameters + +/* @brief This class represents the job that corresponds to loading a clip from disk */ -class ProjectItemModel; -namespace ClipCreator + +class CreateClipJob : public AbstractClipJob { - /* @brief Create and inserts a color clip - @param color : a string of the form "0xff0000ff" (solid red in RGBA) - @param duration : duration expressed in number of frames - @param name: name of the clip - @param parentFolder: the binId of the containing folder - @param model: a shared pointer to the bin item model - @return the binId of the created clip - */ - QString createColorClip(const QString& color, int duration, const QString& name, const QString& parentFolder, std::shared_ptr model); -} + Q_OBJECT + +public: + CreateClipJob(const QString &binId); + + const QString getDescription() const override; + + + void 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 */ + void commitResult(Fun &undo, Fun &redo) override; + +}; #endif diff --git a/src/project/jobs/cutclipjob.cpp b/src/jobs/cutclipjob.cpp similarity index 100% rename from src/project/jobs/cutclipjob.cpp rename to src/jobs/cutclipjob.cpp diff --git a/src/project/jobs/cutclipjob.h b/src/jobs/cutclipjob.h similarity index 100% rename from src/project/jobs/cutclipjob.h rename to src/jobs/cutclipjob.h diff --git a/src/project/jobs/filterjob.cpp b/src/jobs/filterjob.cpp similarity index 97% rename from src/project/jobs/filterjob.cpp rename to src/jobs/filterjob.cpp index 9766d588c..c4c958b2d 100644 --- a/src/project/jobs/filterjob.cpp +++ b/src/jobs/filterjob.cpp @@ -1,262 +1,265 @@ /*************************************************************************** * * * Copyright (C) 2015 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 "filterjob.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" +#include "bin/projectitemmodel.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "meltjob.h" #include "project/clipstabilize.h" #include "project/dialogs/clipspeed.h" #include "ui_scenecutdialog_ui.h" #include #include #include #include // static QList FilterJob::filterClips(const QList &clips, const QStringList ¶ms) { QString condition; if (params.count() > 3) { // there is a condition for this job, for example operate only on vcodec=mpeg1video condition = params.at(3); } QList result; for (int i = 0; i < clips.count(); i++) { ProjectClip *clip = clips.at(i); ClipType type = clip->clipType(); if (type != AV && type != Audio && type != Video) { // Clip will not be processed by this job continue; } if (!condition.isEmpty() && !clip->matches(condition)) { // Clip does not match requested condition, do not process continue; } result << clip; } return result; } QHash FilterJob::prepareJob(const QList &clips, const QStringList ¶meters) { QHash jobs; QStringList sources; sources.reserve(clips.count()); for (int i = 0; i < clips.count(); i++) { sources << clips.at(i)->url(); } const QString filterName = parameters.constFirst(); if (filterName == QLatin1String("timewarp")) { QMap producerParams = QMap(); QMap filterParams = QMap(); QMap consumerParams = QMap(); QMap extraParams = QMap(); producerParams.insert(QStringLiteral("in"), QStringLiteral("0")); producerParams.insert(QStringLiteral("out"), QStringLiteral("-1")); extraParams.insert(QStringLiteral("projecttreefilter"), QStringLiteral("1")); // Reverse clip using project profile since playlists can only be included with same fps // extraParams.insert(QStringLiteral("producer_profile"), QStringLiteral("1")); bool multipleSelection = clips.count() > 1; - QPointer d = new ClipSpeed(clips.count() == 1 ? QUrl::fromLocalFile(sources.constFirst() + QStringLiteral(".mlt")) : QUrl::fromLocalFile(sources.constFirst()).adjusted(QUrl::RemoveFilename), multipleSelection, QApplication::activeWindow()); + QPointer d = new ClipSpeed(clips.count() == 1 ? QUrl::fromLocalFile(sources.constFirst() + QStringLiteral(".mlt")) + : QUrl::fromLocalFile(sources.constFirst()).adjusted(QUrl::RemoveFilename), + multipleSelection, QApplication::activeWindow()); if (d->exec() == QDialog::Accepted) { QLocale locale; QString speedString = QStringLiteral("timewarp:%1:").arg(locale.toString(d->speed() / 100)); QDir destFolder; if (multipleSelection) { destFolder = QDir(d->selectedUrl().toLocalFile()); } for (int i = 0; i < clips.count(); i++) { QString prodstring = speedString + sources.at(i); producerParams.insert(QStringLiteral("producer"), prodstring); QString destination; if (multipleSelection) { destination = destFolder.absoluteFilePath(QUrl::fromLocalFile(sources.at(i)).fileName() + QStringLiteral(".mlt")); } else { destination = d->selectedUrl().toLocalFile(); } if (QFile::exists(destination)) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("File %1 already exists.\nDo you want to overwrite it?", destination)) != KMessageBox::Yes) { continue; } } consumerParams.insert(QStringLiteral("consumer"), QStringLiteral("xml:") + destination); ProjectClip *clip = clips.at(i); auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->description = i18n("Reverse clip"); job->setAddClipToProject(1); jobs.insert(clip, job); } } delete d; return jobs; } if (filterName == QLatin1String("motion_est")) { // Show config dialog QPointer d = new QDialog(QApplication::activeWindow()); Ui::SceneCutDialog_UI ui; ui.setupUi(d); // 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()); if (d->exec() != QDialog::Accepted) { delete d; return jobs; } // Autosplit filter QMap producerParams = QMap(); QMap filterParams = QMap(); QMap consumerParams = QMap(); // Producer params // None // Filter params, use a smaller region of the image to speed up operation // In fact, it's faster to rescale whole image than using part of it (bounding=\"25%x25%:15%x15\") filterParams.insert(QStringLiteral("filter"), filterName); filterParams.insert(QStringLiteral("shot_change_list"), QStringLiteral("0")); filterParams.insert(QStringLiteral("denoise"), QStringLiteral("0")); // Consumer consumerParams.insert(QStringLiteral("consumer"), QStringLiteral("null")); consumerParams.insert(QStringLiteral("all"), QStringLiteral("1")); consumerParams.insert(QStringLiteral("terminate_on_pause"), QStringLiteral("1")); consumerParams.insert(QStringLiteral("real_time"), QStringLiteral("-1")); // We just want to find scene change, set all mathods to the fastests consumerParams.insert(QStringLiteral("rescale"), QStringLiteral("nearest")); consumerParams.insert(QStringLiteral("deinterlace_method"), QStringLiteral("onefield")); consumerParams.insert(QStringLiteral("top_field_first"), QStringLiteral("-1")); // Extra QMap extraParams; extraParams.insert(QStringLiteral("key"), QStringLiteral("shot_change_list")); extraParams.insert(QStringLiteral("projecttreefilter"), QStringLiteral("1")); QString keyword(QStringLiteral("%count")); extraParams.insert(QStringLiteral("resultmessage"), i18n("Found %1 scenes.", keyword)); extraParams.insert(QStringLiteral("resize_profile"), QStringLiteral("160")); if (ui.store_data->isChecked()) { // We want to save result as clip metadata extraParams.insert(QStringLiteral("storedata"), QStringLiteral("1")); } if (ui.zone_only->isChecked()) { // We want to analyze only clip zone extraParams.insert(QStringLiteral("zoneonly"), QStringLiteral("1")); } if (ui.add_markers->isChecked()) { // We want to create markers extraParams.insert(QStringLiteral("addmarkers"), QString::number(ui.marker_type->currentIndex())); extraParams.insert(QStringLiteral("label"), i18n("Scene ")); } if (ui.cut_scenes->isChecked()) { // We want to cut scenes extraParams.insert(QStringLiteral("cutscenes"), QStringLiteral("1")); } delete d; for (int i = 0; i < clips.count(); i++) { // Set clip specific infos // in and out int in = 0; int out = -1; ProjectClip *clip = clips.at(i); if (extraParams.contains(QStringLiteral("zoneonly"))) { // Analyse clip zone only, remove in / out and replace with zone QPoint zone = clip->zone(); in = zone.x(); out = zone.y(); } producerParams.insert(QStringLiteral("in"), QString::number(in)); producerParams.insert(QStringLiteral("out"), QString::number(out)); producerParams.insert(QStringLiteral("producer"), sources.at(i)); // Destination // Since this job is only doing analysis, we have a null consumer and no destination auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->description = i18n("Auto split"); jobs.insert(clip, job); } return jobs; } if (filterName == QLatin1String("vidstab") || filterName == QLatin1String("videostab2") || filterName == QLatin1String("videostab")) { // vidstab int out = 100000; if (clips.count() == 1) { out = clips.constFirst()->duration().frames(KdenliveSettings::project_fps()); } QPointer d = new ClipStabilize(sources, filterName, out); if (d->exec() == QDialog::Accepted) { QMap producerParams = d->producerParams(); QMap filterParams = d->filterParams(); QMap consumerParams = d->consumerParams(); QMap extraParams; extraParams.insert(QStringLiteral("producer_profile"), QStringLiteral("1")); QString destination = d->destination(); QUrl trffile; for (int i = 0; i < clips.count(); i++) { // Set clip specific infos // in and out int clip_in = 0; int clip_out = -1; ProjectClip *clip = clips.at(i); if (extraParams.contains(QStringLiteral("zoneonly"))) { // Analyse clip zone only, remove in / out and replace with zone QPoint zone = clip->zone(); clip_in = zone.x(); clip_out = zone.y(); } producerParams.insert(QStringLiteral("in"), QString::number(clip_in)); producerParams.insert(QStringLiteral("out"), QString::number(clip_out)); producerParams.insert(QStringLiteral("producer"), sources.at(i)); // Consumer QString consumerName = d->consumerParams().value(QStringLiteral("consumer")); if (clips.count() == 1) { // We only have one clip, destination points to the final url consumerParams.insert(QStringLiteral("consumer"), consumerName + QLatin1Char(':') + destination); trffile = QUrl::fromLocalFile(destination + QStringLiteral(".trf")); } else { // Filter several clips, destination points to a folder QString mltfile = destination + QFileInfo(clip->url()).fileName() + QStringLiteral(".mlt"); consumerParams.insert(QStringLiteral("consumer"), consumerName + QLatin1Char(':') + mltfile); trffile = QUrl::fromLocalFile(mltfile + QStringLiteral(".trf")); } consumerParams.insert(QStringLiteral("real_time"), QStringLiteral("-1")); // Append a 'filename' parameter for saving vidstab data filterParams.insert(QStringLiteral("filename"), trffile.toLocalFile()); auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->setAddClipToProject(d->autoAddClip() ? clip->parent()->AbstractProjectItem::clipId().toInt() : -100); job->description = d->desc(); jobs.insert(clip, job); } } delete d; return jobs; } return jobs; } diff --git a/src/project/jobs/filterjob.h b/src/jobs/filterjob.h similarity index 100% rename from src/project/jobs/filterjob.h rename to src/jobs/filterjob.h diff --git a/src/jobs/jobmanager.cpp b/src/jobs/jobmanager.cpp new file mode 100644 index 000000000..ed2630118 --- /dev/null +++ b/src/jobs/jobmanager.cpp @@ -0,0 +1,341 @@ +/* +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/projectitemmodel.h" +#include "core.h" +#include "macros.hpp" +#include "undohelper.hpp" + +#include +#include +#include + +int JobManager::m_currentId = 0; +JobManager::JobManager() + : QAbstractListModel() + , m_lock(QReadWriteLock::Recursive) +{ +} + +JobManager::~JobManager() +{ + slotCancelJobs(); +} + +std::vector JobManager::getPendingJobsIds(const QString &id, AbstractClipJob::JOBTYPE type) +{ + READ_LOCK(); + std::vector result; + 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; + 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(); + 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()) { + 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); + 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, const std::vector &parents) +{ + qDebug() << "################### Createq JOB"<m_id; + bool ok = false; + // wait for parents to finish + while (!ok) { + ok = true; + for (int p : parents) { + if (!m_jobs[p]->m_completionMutex.try_lock()) { + ok = false; + break; + } else { + m_jobs[p]->m_completionMutex.unlock(); + } + } + if (!ok) { + QThread::sleep(1); + } + } + qDebug() << "################### Create JOB STARTING"<m_id; + // connect progress signals + 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); + }); + } + QWriteLocker locker(&m_lock); + job->m_actualFuture = QtConcurrent::mapped(job->m_job, AbstractClipJob::execute); + job->m_future.setFuture(job->m_actualFuture); + 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); }); + connect(&job->m_future, &QFutureWatcher::started, this, &JobManager::updateJobCount); + connect(&job->m_future, &QFutureWatcher::finished, this, &JobManager::updateJobCount); + connect(&job->m_future, &QFutureWatcher::canceled, this, &JobManager::updateJobCount); + + // 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(); + } + if (job->m_future.isCanceled()) { + emit job->m_future.canceled(); + } +} + +void JobManager::slotManageCanceledJob(int id) +{ + QWriteLocker 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); + } + updateJobCount(); +} +void JobManager::slotManageFinishedJob(int id) +{ + qDebug() << "################### JOB finished"< 0); + if (m_jobs[id]->m_processed) return; + m_jobs[id]->m_processed = true; + // send notification to refresh view + for (const auto &it : m_jobs[id]->m_indices) { + pCore->projectItemModel()->onItemUpdated(it.first); + } + bool ok = true; + for (bool res : m_jobs[id]->m_future.future()) { + ok = ok && res; + } + if (!ok) return; + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + for (const auto &j : m_jobs[id]->m_job) { + ok = ok && j->commitResult(undo, redo); + } + if (!ok) { + qDebug() << "ERROR: Job " << id << " failed"; + m_jobs[id]->m_failed = true; + } + if (ok && !m_jobs[id]->m_undoString.isEmpty()) { + pCore->pushUndo(undo, redo, m_jobs[id]->m_undoString); + } + m_jobs[id]->m_completionMutex.unlock(); + updateJobCount(); +} + +AbstractClipJob::JOBTYPE JobManager::getJobType(int jobId) const +{ + READ_LOCK(); + Q_ASSERT(m_jobs.count(jobId) > 0); + return m_jobs.at(jobId)->m_type; +} + +JobStatus 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 JobStatus::Finished; + } + if (job->m_future.isCanceled()) { + return JobStatus::Canceled; + } + if (job->m_future.isRunning()) { + return JobStatus::Running; + } + return JobStatus::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/jobmanager.h b/src/jobs/jobmanager.h new file mode 100644 index 000000000..0330e0e06 --- /dev/null +++ b/src/jobs/jobmanager.h @@ -0,0 +1,170 @@ +/* +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 JOBMANAGER +#define JOBMANAGER + +#include "abstractclipjob.h" +#include "definitions.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +class AbstractClipJob; + +/** + * @class JobManager + * @brief This class is responsible for clip jobs management. + * + */ + +enum class JobStatus { NoJob, Pending, Running, Finished, Canceled }; +Q_DECLARE_METATYPE(JobStatus) +struct Job_t +{ + std::vector> m_job; // List of the jobs + std::vector m_progress; // progress of the job, for each clip + std::unordered_map m_indices; // keys are binIds, value are ids in the vectors m_job and m_progress; + QFutureWatcher m_future; // future of the job + QFuture m_actualFuture; + QMutex m_completionMutex; // mutex that is locked during execution of the process + AbstractClipJob::JOBTYPE m_type; + QString m_undoString; + int m_id; + bool m_processed = false; // flag that we set to true when we are done with this job + bool m_failed = false; // flag that we set to true when a problem occured +}; + +class AudioThumbJob; +class LoadJob; +class SceneSplitJob; +class StabilizeJob; +class ThumbJob; + +class JobManager : public QAbstractListModel, public enable_shared_from_this_virtual +{ + Q_OBJECT + +public: + explicit JobManager(); + virtual ~JobManager(); + + /** @brief Start a job + This function calls the prepareJob function of the job if it provides one. + @param T is the type of job (must inherit from AbstractClipJob) + @param binIds is the list of clips to which we apply the job + @param parents is the list of the ids of the job that must terminate before this one can start + @param args are the arguments to construct the job + @param return the id of the created job + */ + template + int startJob(const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args); + // Same function, but we specify the function used to create a new job + template + int startJob(const std::vector &binIds, const std::vector &parents, QString undoString, + std::function(const QString &, Args...)> createFn, Args &&... args); + + // Same function, but do not call prepareJob + template + int startJob_noprepare(const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args); + + /** @brief Discard specific job type for a clip. + * @param binId the clip id + * @param type The type of job that you want to abort, leave to NOJOBTYPE to abort all jobs + */ + void discardJobs(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); + + /** @brief Check if there is a pending / running job a clip. + * @param binId the clip id + * @param type The type of job that you want to query + * @param foundId : if a valid ptr is passed, we store the id of the first matching job found (-1 if not found) + */ + bool hasPendingJob(const QString &binId, AbstractClipJob::JOBTYPE type, int *foundId = nullptr); + + /** @brief Get the list of pending or running job ids for given clip. + * @param binId the clip id + * @param type The type of job that you want to query. Leave to NOJOBTYPE to match all + */ + std::vector getPendingJobsIds(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); + + /** @brief Get the list of finished or cancelled job ids for given clip. + * @param binId the clip id + * @param type The type of job that you want to query. Leave to NOJOBTYPE to match all + */ + std::vector getFinishedJobsIds(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); + + /** @brief return the type of a given job */ + AbstractClipJob::JOBTYPE getJobType(int jobId) const; + + /** @brief return the type of a given job */ + JobStatus getJobStatus(int jobId) const; + + /** @brief return the progress of a given job on a given clip */ + int getJobProgressForClip(int jobId, const QString &binId) const; + + /** @brief return the message of a given job on a given clip */ + QString getJobMessageForClip(int jobId, const QString &binId) const; + + // Mandatory overloads + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + // Helper function to launch a given job. + // This has to be launched asynchrnously since it blocks until all parents are finished + void createJob(std::shared_ptr job, const std::vector &parents); + + void updateJobCount(); + + void slotManageCanceledJob(int id); + void slotManageFinishedJob(int id); + +public slots: + /** @brief Discard jobs running on a given clip */ + void slotDiscardClipJobs(const QString &binId); + /** @brief Discard all running jobs. */ + void slotCancelJobs(); + /** @brief Discard all pending jobs. */ + void slotCancelPendingJobs(); + +private: + /** @brief This is a lock that ensures safety in case of concurrent access */ + mutable QReadWriteLock m_lock; + /** @brief This is the id of the last created job */ + static int m_currentId; + /** @brief This is the list of all jobs, ordered by id. A job is represented by a pointer to the job class and a future to the result */ + std::map> m_jobs; + /** @brief List of all the jobs by clip. */ + std::unordered_map> m_jobsByClip; + +signals: + void jobCount(int); +}; + +#include "jobmanager.ipp" +#endif diff --git a/src/jobs/jobmanager.ipp b/src/jobs/jobmanager.ipp new file mode 100644 index 000000000..b979736b7 --- /dev/null +++ b/src/jobs/jobmanager.ipp @@ -0,0 +1,112 @@ +/* +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 +#include +template +int JobManager::startJob(const std::vector &binIds, const std::vector &parents, QString undoString, + std::function(const QString &, Args...)> createFn, Args &&... args) +{ + static_assert(std::is_base_of::value, "Your job must inherit from AbstractClipJob"); + QWriteLocker locker(&m_lock); + int jobId = m_currentId++; + std::shared_ptr job(new Job_t()); + job->m_undoString = std::move(undoString); + job->m_id = jobId; + for (const auto &id : binIds) { + job->m_job.push_back(createFn(id, args...)); + job->m_progress.push_back(0); + job->m_indices[id] = size_t(int(job->m_job.size()) - 1); + job->m_type = job->m_job.back()->jobType(); + m_jobsByClip[id].push_back(jobId); + } + job->m_completionMutex.lock(); + int insertionRow = static_cast(m_jobs.size()); + beginInsertRows(QModelIndex(), insertionRow, insertionRow); + Q_ASSERT(m_jobs.count(jobId) == 0); + m_jobs[jobId] = job; + endInsertRows(); + QtConcurrent::run(this, &JobManager::createJob, job, parents); + return jobId; +} + +// we must specialize the second version of startjob depending on the type (some types requires to use a prepareJob method). Because we cannot use partial +// specialization for functions, we resort to a static method of a class in this impl namespace we must specialize the second version of startjob depending on +// the type (some types requires to use a prepareJob method). Because we cannot use partial specialization for functions, we resort to a static method of a +// dummy struct in a namespace + +namespace impl { + +// This is a simple member detector borrowed from https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Member_Detector +template class Detect_prepareJob +{ + // clang-format off + struct Fallback {int prepareJob;}; // add member name "prepareJob" + struct Derived : T, Fallback {}; + // clang-format on + + template struct Check; + + typedef char ArrayOfOne[1]; // typedef for an array of size one. + typedef char ArrayOfTwo[2]; // typedef for an array of size two. + + template static ArrayOfOne &func(Check *); + template static ArrayOfTwo &func(...); + +public: + typedef Detect_prepareJob type; + enum { value = sizeof(func(0)) == 2 }; +}; + +struct dummy +{ + + template + static typename std::enable_if::value || Noprepare, int>::type + exec(std::shared_ptr ptr, const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args) + { + auto defaultCreate = [](const QString &id, Args... local_args) { return AbstractClipJob::make(id, std::forward(local_args)...); }; + using local_createFn_t = std::function(const QString &, Args...)>; + return ptr->startJob(binIds, parents, std::move(undoString), local_createFn_t(std::move(defaultCreate)), std::forward(args)...); + } + template + static typename std::enable_if::value && !Noprepare, int>::type + exec(std::shared_ptr ptr, const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args) + { + // For job stabilization, there is a custom preparation function + return T::prepareJob(ptr, binIds, parents, std::move(undoString), std::forward(args)...); + } +}; + +} // namespace impl + +template +int JobManager::startJob(const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args) +{ + return impl::dummy::exec(shared_from_this(), binIds, parents, std::move(undoString), std::forward(args)...); +} + +template +int JobManager::startJob_noprepare(const std::vector &binIds, const std::vector &parents, QString undoString, Args &&... args) +{ + return impl::dummy::exec(shared_from_this(), binIds, parents, std::move(undoString), std::forward(args)...); +} diff --git a/src/jobs/loadjob.cpp b/src/jobs/loadjob.cpp new file mode 100644 index 000000000..c395995a8 --- /dev/null +++ b/src/jobs/loadjob.cpp @@ -0,0 +1,535 @@ +/*************************************************************************** + * 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 + +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 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()) { + 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 + 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 != projectProfile) { + // Profiles do not match, propose profile adjustment + pCore->currentDoc()->switchProfile(projectProfile, m_clipId, m_xml); + } else if (KdenliveSettings::default_profile().isEmpty()) { + // Confirm default project format + KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); + } + } +} + +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; + } + QString m_resource = Xml::getXmlProperty(m_xml, QStringLiteral("resource")); + ClipType 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; + 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()) { + 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 && qAbs(originalFps - pCore->getCurrentFps()) > 1e-4) { + 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 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; }; + + 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); + } + 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); + } + } + } + pCore->pushUndo(undo, redo, i18n("Add additional streams for clip")); +} + +bool LoadJob::commitResult(Fun &undo, Fun &redo) +{ + qDebug() << "################### loadjob COMMIT"; + Q_ASSERT(!m_resultConsumed); + if (!m_done) { + qDebug() << "ERROR: Trying to consume invalid results"; + return false; + } + m_resultConsumed = true; + if (!m_successful) { + return false; + } + if (m_xml.hasAttribute(QStringLiteral("checkProfile")) && m_producer->get_int("video_index") > -1) { + checkProfile(); + } + if (m_video_list.size() > 1) { + processMultiStream(); + } + + auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); + // 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) { + UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); + } + return ok; +} diff --git a/src/jobs/loadjob.hpp b/src/jobs/loadjob.hpp new file mode 100644 index 000000000..5d4de2c41 --- /dev/null +++ b/src/jobs/loadjob.hpp @@ -0,0 +1,79 @@ +/*************************************************************************** + * 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; + +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/project/jobs/meltjob.cpp b/src/jobs/meltjob.cpp similarity index 62% rename from src/project/jobs/meltjob.cpp rename to src/jobs/meltjob.cpp index a04d58adf..b532d3ad3 100644 --- a/src/project/jobs/meltjob.cpp +++ b/src/jobs/meltjob.cpp @@ -1,291 +1,279 @@ /*************************************************************************** - * * * 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) any later version. * + * (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, write to the * - * Free Software Foundation, Inc., * - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * + * along with this program. If not, see . * ***************************************************************************/ #include "meltjob.h" +#include "bin/projectclip.h" +#include "bin/projectitemmodel.h" #include "core.h" -#include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include #include static void consumer_frame_render(mlt_consumer, MeltJob *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); - self->emitFrameNumber((int)frame.get_position()); + self->mltFrameCallback((int)frame.get_position()); } -MeltJob::MeltJob(ClipType cType, const QString &id, const QMap &producerParams, const QMap &filterParams, - const QMap &consumerParams, const QMap &extraParams) - : AbstractClipJob(MLTJOB, cType, id) - , addClipToProject(0) - , m_consumer(nullptr) - , m_producer(nullptr) - , m_profile(nullptr) - , m_filter(nullptr) - , m_showFrameEvent(nullptr) - , m_producerParams(producerParams) - , m_filterParams(filterParams) - , m_consumerParams(consumerParams) - , m_length(0) - , m_extra(extraParams) +MeltJob::MeltJob(const QString &binId, JOBTYPE type, bool useProducerProfile, int in, int out) + : AbstractClipJob(type, binId) + , m_profile(pCore->getCurrentProfile()->profile()) + , m_useProducerProfile(useProducerProfile) + , m_in(in) + , m_out(out) { - m_jobStatus = JobWaiting; - description = i18n("Processing clip"); - QString consum = m_consumerParams.value(QStringLiteral("consumer")); - if (consum.contains(QLatin1Char(':'))) { - m_dest = consum.section(QLatin1Char(':'), 1); - } - m_url = producerParams.value(QStringLiteral("producer")); } -void MeltJob::startJob() +bool MeltJob::startJob() { + auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); + m_url = binClip->url(); if (m_url.isEmpty()) { m_errorMessage.append(i18n("No producer for this clip.")); - setStatus(JobCrashed); - return; + m_successful = false; + m_done = true; + return false; } + /* QString consumerName = m_consumerParams.value(QStringLiteral("consumer")); // safety check, make sure we don't overwrite a source clip if (!m_dest.isEmpty() && !m_dest.endsWith(QStringLiteral(".mlt"))) { m_errorMessage.append(i18n("Invalid destination: %1.", consumerName)); setStatus(JobCrashed); return; } int in = m_producerParams.value(QStringLiteral("in")).toInt(); if (in > 0 && !m_extra.contains(QStringLiteral("offset"))) { m_extra.insert(QStringLiteral("offset"), QString::number(in)); } int out = m_producerParams.value(QStringLiteral("out")).toInt(); QString filterName = m_filterParams.value(QStringLiteral("filter")); // optional params int startPos = -1; int track = -1; // used when triggering a job from an effect if (m_extra.contains(QStringLiteral("clipStartPos"))) { startPos = m_extra.value(QStringLiteral("clipStartPos")).toInt(); } if (m_extra.contains(QStringLiteral("clipTrack"))) { track = m_extra.value(QStringLiteral("clipTrack")).toInt(); } if (!m_extra.contains(QStringLiteral("finalfilter"))) { m_extra.insert(QStringLiteral("finalfilter"), filterName); } if (out != -1 && out <= in) { m_errorMessage.append(i18n("Clip zone undefined (%1 - %2).", in, out)); setStatus(JobCrashed); return; } - Mlt::Profile *projectProfile = new Mlt::Profile(pCore->getCurrentProfile()->profile()); - bool producerProfile = m_extra.contains(QStringLiteral("producer_profile")); - if (producerProfile) { - m_profile = new Mlt::Profile; - m_profile->set_explicit(0); + */ + auto &projectProfile = pCore->getCurrentProfile(); + Mlt::Profile producerProfile; + // bool producerProfile = m_extra.contains(QStringLiteral("producer_profile")); + if (m_useProducerProfile) { + m_profile = producerProfile; + m_profile.set_explicit(0); } else { - m_profile = projectProfile; + m_profile = projectProfile->profile(); } + /* if (m_extra.contains(QStringLiteral("resize_profile"))) { m_profile->set_height(m_extra.value(QStringLiteral("resize_profile")).toInt()); m_profile->set_width(m_profile->height() * m_profile->sar()); } + */ double fps = projectProfile->fps(); int fps_num = projectProfile->frame_rate_num(); int fps_den = projectProfile->frame_rate_den(); - Mlt::Producer *producer = new Mlt::Producer(*m_profile, m_url.toUtf8().constData()); - if ((producer != nullptr) && producerProfile) { - m_profile->from_producer(*producer); - m_profile->set_explicit(1); + + m_producer.reset(new Mlt::Producer(m_profile, m_url.toUtf8().constData())); + if (m_producer && m_useProducerProfile) { + m_profile.from_producer(*m_producer.get()); + m_profile.set_explicit(1); } - if (qAbs(m_profile->fps() - fps) > 0.01 || producerProfile) { + configureProfile(); + if (qAbs(m_profile.fps() - fps) > 0.01 || m_useProducerProfile) { // Reload producer - delete producer; // Force same fps as projec profile or the resulting .mlt will not load in our project - m_profile->set_frame_rate(fps_num, fps_den); - producer = new Mlt::Producer(*m_profile, m_url.toUtf8().constData()); - } - if (producerProfile) { - delete projectProfile; + m_profile.set_frame_rate(fps_num, fps_den); + m_producer.reset(new Mlt::Producer(m_profile, m_url.toUtf8().constData())); } - if ((producer == nullptr) || !producer->is_valid()) { + if ((m_producer == nullptr) || !m_producer->is_valid()) { // Clip was removed or something went wrong, Notify user? - // m_errorMessage.append(i18n("Invalid clip")); - setStatus(JobCrashed); - return; + m_errorMessage.append(i18n("Invalid clip")); + m_successful = false; + m_done = true; + return false; } + /* // Process producer params QMapIterator i(m_producerParams); QStringList ignoredProps; ignoredProps << QStringLiteral("producer") << QStringLiteral("in") << QStringLiteral("out"); while (i.hasNext()) { i.next(); QString key = i.key(); if (!ignoredProps.contains(key)) { producer->set(i.key().toUtf8().constData(), i.value().toUtf8().constData()); } } + */ - if (out == -1 && in == -1) { - m_producer = producer; - } else { - m_producer = producer->cut(in, out); - delete producer; + if (m_out == -1) { + m_out = m_producer->get_playtime() - 1; + } + if (m_in == -1) { + m_in = 0; + } + if (m_out != m_producer->get_playtime() - 1 || m_in != 0) { + std::swap(m_wholeProducer, m_producer); + m_producer.reset(m_wholeProducer->cut(m_in, m_out)); + } + configureProducer(); + if ((m_producer == nullptr) || !m_producer->is_valid()) { + // Clip was removed or something went wrong, Notify user? + m_errorMessage.append(i18n("Invalid clip")); + m_successful = false; + m_done = true; + return false; } // Build consumer - if (consumerName.contains(QLatin1String(":"))) { - m_consumer = new Mlt::Consumer(*m_profile, consumerName.section(QLatin1Char(':'), 0, 0).toUtf8().constData(), m_dest.toUtf8().constData()); + configureConsumer(); + /* + if (m_consumerName.contains(QLatin1String(":"))) { + m_consumer.reset(new Mlt::Consumer(*m_profile, consumerName.section(QLatin1Char(':'), 0, 0).toUtf8().constData(), m_dest.toUtf8().constData())); } else { m_consumer = new Mlt::Consumer(*m_profile, consumerName.toUtf8().constData()); - } + }*/ if ((m_consumer == nullptr) || !m_consumer->is_valid()) { - m_errorMessage.append(i18n("Cannot create consumer %1.", consumerName)); - setStatus(JobCrashed); - return; + m_errorMessage.append(i18n("Cannot create consumer.")); + m_successful = false; + m_done = true; + return false; } + /* if (!m_consumerParams.contains(QStringLiteral("real_time"))) { m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } + */ + // Process consumer params + /* QMapIterator j(m_consumerParams); ignoredProps.clear(); ignoredProps << QStringLiteral("consumer"); while (j.hasNext()) { j.next(); QString key = j.key(); if (!ignoredProps.contains(key)) { m_consumer->set(j.key().toUtf8().constData(), j.value().toUtf8().constData()); } } if (consumerName.startsWith(QStringLiteral("xml:"))) { // Use relative path in xml m_consumer->set("root", QFileInfo(m_dest).absolutePath().toUtf8().constData()); } + */ // Build filter + configureFilter(); + /* if (!filterName.isEmpty()) { m_filter = new Mlt::Filter(*m_profile, filterName.toUtf8().data()); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage = i18n("Filter %1 crashed", filterName); setStatus(JobCrashed); return; } // Process filter params QMapIterator k(m_filterParams); ignoredProps.clear(); ignoredProps << QStringLiteral("filter"); while (k.hasNext()) { k.next(); QString key = k.key(); if (!ignoredProps.contains(key)) { m_filter->set(k.key().toUtf8().constData(), k.value().toUtf8().constData()); } } } - Mlt::Tractor tractor(*m_profile); + */ + if ((m_filter == nullptr) || !m_filter->is_valid()) { + m_errorMessage.append(i18n("Cannot create filter.")); + m_successful = false; + m_done = true; + return false; + } + + Mlt::Tractor tractor(m_profile); Mlt::Playlist playlist; - playlist.append(*m_producer); + playlist.append(*m_producer.get()); tractor.set_track(playlist, 0); m_consumer->connect(tractor); m_producer->set_speed(0); m_producer->seek(0); m_length = m_producer->get_playtime(); if (m_length == 0) { m_length = m_producer->get_length(); } if (m_filter) { - m_producer->attach(*m_filter); + m_producer->attach(*m_filter.get()); } - m_showFrameEvent = m_consumer->listen("consumer-frame-render", this, (mlt_listener)consumer_frame_render); + m_showFrameEvent.reset(m_consumer->listen("consumer-frame-render", this, (mlt_listener)consumer_frame_render)); m_producer->set_speed(1); m_consumer->run(); + /* QMap jobResults; if (m_jobStatus != JobAborted && m_extra.contains(QStringLiteral("key"))) { QString result = QString::fromLatin1(m_filter->get(m_extra.value(QStringLiteral("key")).toUtf8().constData())); jobResults.insert(m_extra.value(QStringLiteral("key")), result); } if (!jobResults.isEmpty() && m_jobStatus != JobAborted) { emit gotFilterJobResults(m_clipId, startPos, track, jobResults, m_extra); } if (m_jobStatus == JobWorking) { m_jobStatus = JobDone; } + */ + m_successful = m_done = true; + return true; } -MeltJob::~MeltJob() -{ - delete m_showFrameEvent; - delete m_filter; - delete m_producer; - delete m_consumer; - delete m_profile; -} - -const QString MeltJob::destination() const -{ - return m_dest; -} - -stringMap MeltJob::cancelProperties() -{ - QMap props; - return props; -} - -const QString MeltJob::statusMessage() -{ - QString statusInfo; - switch (m_jobStatus) { - case JobWorking: - statusInfo = description; - break; - case JobWaiting: - statusInfo = i18n("Waiting to process clip"); - break; - default: - break; - } - return statusInfo; -} - -void MeltJob::emitFrameNumber(int pos) -{ - if (m_length > 0 && m_jobStatus == JobWorking) { - emit jobProgress(m_clipId, (int)(100 * pos / m_length), jobType); - } -} - -void MeltJob::setStatus(ClipJobStatus status) +void MeltJob::mltFrameCallback(int pos) { - m_jobStatus = status; - if (status == JobAborted && (m_consumer != nullptr)) { - m_consumer->stop(); + if (m_length > 0) { + emit jobProgress((int)(100 * pos / m_length)); } } diff --git a/src/jobs/meltjob.h b/src/jobs/meltjob.h new file mode 100644 index 000000000..34cddd8ea --- /dev/null +++ b/src/jobs/meltjob.h @@ -0,0 +1,90 @@ +/*************************************************************************** + * 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 . * + ***************************************************************************/ + +#ifndef MELTJOB +#define MELTJOB + +#include "abstractclipjob.h" + +namespace Mlt { +class Profile; +class Producer; +class Consumer; +class Filter; +class Event; +} // namespace Mlt + +/** + * @class MeltJob + * @brief This is an abstract class for jobs that rely on a melt filter to process the clips + * + */ + +class MeltJob : public AbstractClipJob +{ + Q_OBJECT + +public: + /** @brief Creates a melt job for the given bin clip + consumerName is the melt name of the consumer that we use + if useProducerProfile == true, the profile used will be the one of the producer + in and out represent the portion of the clip we deal with. Leave to -1 for default (whole clip) + */ + MeltJob(const QString &binId, JOBTYPE type, bool useProducerProfile = false, int in = -1, int out = -1); + + bool startJob() override; + + /* @brief this is public for convenience reason, but it should not be called directly */ + void mltFrameCallback(int frame); + +protected: + // @brief extra configuration of the profile (eg: resize the profile) + virtual void configureProfile() {} + + // @brief extra configuration of the producer + virtual void configureProducer() {} + + // @brief create and configure consumer + virtual void configureConsumer() = 0; + + // @brief create and configure filter + virtual void configureFilter() = 0; + +protected: + std::unique_ptr m_consumer; + std::unique_ptr m_producer; + std::unique_ptr m_wholeProducer; // in the case of a job on a part on the clip, this is set to the whole producer + Mlt::Profile &m_profile; + std::unique_ptr m_filter; + std::unique_ptr m_showFrameEvent; + + bool m_done{false}, m_successful{false}; + + QString m_url; + QString m_filterName; + bool m_useProducerProfile; + int m_in, m_out; + int m_length; +}; + +#endif diff --git a/src/project/jobs/proxyclipjob.cpp b/src/jobs/proxyclipjob.cpp similarity index 100% rename from src/project/jobs/proxyclipjob.cpp rename to src/jobs/proxyclipjob.cpp diff --git a/src/project/jobs/proxyclipjob.h b/src/jobs/proxyclipjob.h similarity index 100% rename from src/project/jobs/proxyclipjob.h rename to src/jobs/proxyclipjob.h diff --git a/src/jobs/scenesplitjob.cpp b/src/jobs/scenesplitjob.cpp new file mode 100644 index 000000000..bd037ca6a --- /dev/null +++ b/src/jobs/scenesplitjob.cpp @@ -0,0 +1,130 @@ +/*************************************************************************** + * 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) + : MeltJob(binId, STABILIZEJOB, true, -1, -1) + , m_subClips(subClips) + , m_markersType(markersType) +{ +} + +const QString SceneSplitJob::getDescription() const +{ + return i18n("Scene split"); +} +void SceneSplitJob::configureConsumer() +{ + m_consumer.reset(new Mlt::Consumer(m_profile, "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 mathods 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, "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, const std::vector &parents, 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(); + + return ptr->startJob_noprepare(binIds, parents, std::move(undoString), subclips, markersType); +} + +bool SceneSplitJob::commitResult(Fun &undo, Fun &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); + 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 new file mode 100644 index 000000000..150951a38 --- /dev/null +++ b/src/jobs/scenesplitjob.hpp @@ -0,0 +1,68 @@ +/*************************************************************************** + * 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); + + // 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, const std::vector &parents, QString undoString); + + bool commitResult(Fun &undo, Fun &redo) 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; +}; diff --git a/src/jobs/stabilizejob.cpp b/src/jobs/stabilizejob.cpp new file mode 100644 index 000000000..5e664caef --- /dev/null +++ b/src/jobs/stabilizejob.cpp @@ -0,0 +1,159 @@ +/*************************************************************************** + * 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, "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, 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, const std::vector &parents, 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, parents, std::move(undoString), local_createFn_t(std::move(createFn))); + } + } + return -1; +} + +bool StabilizeJob::commitResult(Fun &undo, Fun &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; + } + + 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 new file mode 100644 index 000000000..752c55c6e --- /dev/null +++ b/src/jobs/stabilizejob.hpp @@ -0,0 +1,72 @@ +/*************************************************************************** + * 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, const std::vector &parents, 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; + 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 new file mode 100644 index 000000000..dc7a2e91a --- /dev/null +++ b/src/jobs/thumbjob.cpp @@ -0,0 +1,152 @@ +/*************************************************************************** + * 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) + : AbstractClipJob(THUMBJOB, binId) + , m_frameNumber(frameNumber) + , m_fullWidth(imageHeight * pCore->getCurrentDar() + 0.5) + , m_imageHeight(imageHeight) + , m_persistent(persistent) + , 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()); + } else { + m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); + } + m_prod = m_binClip->originalProducer(); + if ((m_prod == nullptr) || !m_prod->is_valid()) { + return false; + } + int max = m_prod->get_length(); + m_frameNumber = std::min(max - 1, m_frameNumber); + + // m_frameNumber = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:thumbnailFrame"), QStringLiteral("-1")).toInt(); + if (ThumbnailCache::get()->hasThumbnail(m_clipId, m_frameNumber, !m_persistent)) { + m_done = true; + m_result = ThumbnailCache::get()->getThumbnail(m_clipId, m_frameNumber); + m_inCache = true; + return true; + } + + 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); + 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) +{ + Q_ASSERT(!m_resultConsumed); + if (!m_done) { + qDebug() << "ERROR: Trying to consume invalid results"; + return false; + } + m_resultConsumed = true; + if (!m_inCache) { + ThumbnailCache::get()->storeThumbnail(m_clipId, m_frameNumber, m_result, m_persistent); + } + + // 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) ]() + { + clip->setThumbnail(image); + return true; + }; + auto reverse = [ clip = m_binClip, image = std::move(old) ]() + { + clip->setThumbnail(image); + return true; + }; + ok = operation(); + if (ok) { + UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); + } + } + return ok; +} diff --git a/src/bin/clipcreator.hpp b/src/jobs/thumbjob.hpp similarity index 56% copy from src/bin/clipcreator.hpp copy to src/jobs/thumbjob.hpp index 0693a797a..f45a10141 100644 --- a/src/bin/clipcreator.hpp +++ b/src/jobs/thumbjob.hpp @@ -1,45 +1,68 @@ /*************************************************************************** * 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 CLIPCREATOR_H -#define CLIPCREATOR_H +#pragma once + +#include "abstractclipjob.h" -#include #include -/** @brief This namespace provides convenienc function to create clips based on various parameters +/* @brief This class represents the job that corresponds to computing the thumb of a clip */ -class ProjectItemModel; -namespace ClipCreator +class ProjectClip; +namespace Mlt { +class Producer; +} + +class ThumbJob : public AbstractClipJob { - /* @brief Create and inserts a color clip - @param color : a string of the form "0xff0000ff" (solid red in RGBA) - @param duration : duration expressed in number of frames - @param name: name of the clip - @param parentFolder: the binId of the containing folder - @param model: a shared pointer to the bin item model - @return the binId of the created clip + 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) */ - QString createColorClip(const QString& color, int duration, const QString& name, const QString& parentFolder, std::shared_ptr model); -} + ThumbJob(const QString &binId, int imageHeight, int frameNumber = -1, bool persistent = 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; + +private: + int m_frameNumber; + int m_fullWidth; + int m_imageHeight; + + std::shared_ptr m_binClip; + std::shared_ptr m_prod; -#endif + QImage m_result; + bool m_done{false}; + bool m_persistent; + bool m_inCache {false}; + bool m_subClip {false}; // true if we operate on a subclip +}; diff --git a/src/macros.hpp b/src/macros.hpp index 48fbe48c1..3a700ceb4 100644 --- a/src/macros.hpp +++ b/src/macros.hpp @@ -1,88 +1,97 @@ /*************************************************************************** * 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 MACROS_H #define MACROS_H /* This file contains a collection of macros that can be used in model related classes. The class only needs to have the following members: - For Push_undo : std::weak_ptr m_undoStack; this is a pointer to the undoStack - For Update_undo_redo: mutable QReadWriteLock m_lock; This is a lock that ensures safety in case of concurrent access. Note that the mutex must be recursive. See for example TimelineModel. + + Note that there also exists a version of update_undo_redo without the need for a lock (but prefer the mutex version where applicable) */ /* This convenience macro adds lock/unlock ability to a given lambda function Note that it is automatically called when you push the lambda so you shouldn't have to call it directly yourself */ #define LOCK_IN_LAMBDA(lambda) \ lambda = [this, lambda]() { \ m_lock.lockForWrite(); \ bool res_lambda = lambda(); \ m_lock.unlock(); \ return res_lambda; \ }; /*This convenience macro locks the mutex for reading. Note that it might happen that a thread is executing a write operation that requires reading a Read-protected property. In that case, we try to write lock it first (this will be granted since the lock is recursive) */ #define READ_LOCK() \ std::unique_ptr rlocker(new QReadLocker(nullptr)); \ std::unique_ptr wlocker(new QWriteLocker(nullptr)); \ if (m_lock.tryLockForWrite()) { \ /*we yield ownership of the lock to the WriteLocker*/ \ m_lock.unlock(); \ wlocker.reset(new QWriteLocker(&m_lock)); \ } else { \ rlocker.reset(new QReadLocker(&m_lock)); \ } /* @brief This macro takes some lambdas that represent undo/redo for an operation and the text (name) associated with this operation The lambdas are transformed to make sure they lock access to the class they operate on. Then they are added on the undoStack */ #define PUSH_UNDO(undo, redo, text) \ if (auto ptr = m_undoStack.lock()) { \ ptr->push(new FunctionalUndoCommand(undo, redo, text)); \ } else { \ qDebug() << "ERROR : unable to access undo stack"; \ Q_ASSERT(false); \ } /* @brief This macro takes as parameter one atomic operation and its reverse, and update the undo and redo functional stacks/queue accordingly - It will also ensure that operation and reverse are dealing with mutexes + This should be used in the rare case where we don't need a lock mutex. In general, prefer the other version */ -#define UPDATE_UNDO_REDO(operation, reverse, undo, redo) \ - LOCK_IN_LAMBDA(operation) \ - LOCK_IN_LAMBDA(reverse) \ +#define UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo) \ undo = [reverse, undo]() { \ bool v = reverse(); \ return undo() && v; \ }; \ redo = [operation, redo]() { \ bool v = redo(); \ return operation() && v; \ }; +/* @brief This macro takes as parameter one atomic operation and its reverse, and update + the undo and redo functional stacks/queue accordingly + It will also ensure that operation and reverse are dealing with mutexes +*/ +#define UPDATE_UNDO_REDO(operation, reverse, undo, redo) \ + LOCK_IN_LAMBDA(operation) \ + LOCK_IN_LAMBDA(reverse) \ + UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo) + #endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 64c773f3d..1f6a52cc8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,4137 +1,4159 @@ /*************************************************************************** * Copyright (C) 2007 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 "mainwindow.h" #include "assets/assetpanel.hpp" +#include "bin/clipcreator.hpp" #include "bin/generators/generators.h" #include "bin/projectclip.h" +#include "bin/projectfolder.h" +#include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "dialogs/kdenlivesettingsdialog.h" #include "dialogs/renderwidget.h" #include "dialogs/wizard.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/view/effectlistwidget.hpp" #include "effectslist/effectbasket.h" #include "effectslist/effectslistview.h" #include "effectslist/effectslistwidget.h" #include "effectslist/initeffects.h" #include "hidetitlebars.h" +#include "jobs/jobmanager.h" +#include "jobs/stabilizejob.hpp" +#include "jobs/scenesplitjob.hpp" #include "kdenlivesettings.h" #include "layoutmanagement.h" #include "library/librarywidget.h" #include "mainwindowadaptor.h" #include "mltconnection.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/producerqueue.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "monitor/scopes/audiographspectrum.h" #include "profiles/profilemodel.hpp" #include "project/clipmanager.h" #include "project/cliptranscode.h" #include "project/dialogs/archivewidget.h" #include "project/dialogs/projectsettings.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "renderer.h" #include "scopes/scopemanager.h" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinetabs.hpp" #include "timeline2/view/timelinewidget.h" #include "titler/titlewidget.h" #include "transitions/transitionlist/view/transitionlistwidget.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/resourcewidget.h" #include "utils/thememanager.h" -#include "widgets/progressbutton.h" -#include #include "effectslist/effectslistwidget.h" #include "profiles/profilerepository.hpp" +#include "widgets/progressbutton.h" +#include #include "project/dialogs/temporarydata.h" #include "utils/KoIconUtils.h" #ifdef USE_JOGSHUTTLE #include "jogshuttle/jogmanager.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char version[] = KDENLIVE_VERSION; namespace Mlt { class Producer; } EffectsList MainWindow::videoEffects; EffectsList MainWindow::audioEffects; EffectsList MainWindow::customEffects; EffectsList MainWindow::transitions; QMap MainWindow::m_lumacache; QMap MainWindow::m_lumaFiles; /*static bool sortByNames(const QPair &a, const QPair &b) { return a.first < b.first; }*/ // determine the default KDE style as defined BY THE USER // (as opposed to whatever style KDE considers default) static QString defaultStyle(const char *fallback = nullptr) { KSharedConfigPtr kdeGlobals = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals); KConfigGroup cg(kdeGlobals, "KDE"); return cg.readEntry("widgetStyle", fallback); } MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent) , m_assetPanel(nullptr) , m_exitCode(EXIT_SUCCESS) , m_effectList(nullptr) , m_transitionList(nullptr) , m_clipMonitor(nullptr) , m_projectMonitor(nullptr) , m_renderWidget(nullptr) , m_messageLabel(nullptr) , m_themeInitialized(false) , m_isDarkTheme(false) { } void MainWindow::init() { // Widget themes for non KDE users KActionMenu *stylesAction = new KActionMenu(i18n("Style"), this); auto *stylesGroup = new QActionGroup(stylesAction); // GTK theme does not work well with Kdenlive, and does not support color theming, so avoid it QStringList availableStyles = QStyleFactory::keys(); QString desktopStyle = QApplication::style()->objectName(); if (KdenliveSettings::widgetstyle().isEmpty()) { // First run QStringList incompatibleStyles; incompatibleStyles << QStringLiteral("GTK+") << QStringLiteral("windowsvista") << QStringLiteral("windowsxp"); if (incompatibleStyles.contains(desktopStyle, Qt::CaseInsensitive)) { if (availableStyles.contains(QStringLiteral("breeze"), Qt::CaseInsensitive)) { // Auto switch to Breeze theme KdenliveSettings::setWidgetstyle(QStringLiteral("Breeze")); } else if (availableStyles.contains(QStringLiteral("fusion"), Qt::CaseInsensitive)) { KdenliveSettings::setWidgetstyle(QStringLiteral("Fusion")); } } } // Add default style action QAction *defaultStyle = new QAction(i18n("Default"), stylesGroup); defaultStyle->setCheckable(true); stylesAction->addAction(defaultStyle); if (KdenliveSettings::widgetstyle().isEmpty()) { defaultStyle->setChecked(true); } for (const QString &style : availableStyles) { auto *a = new QAction(style, stylesGroup); a->setCheckable(true); a->setData(style); if (KdenliveSettings::widgetstyle() == style) { a->setChecked(true); } stylesAction->addAction(a); } connect(stylesGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeStyle); // QIcon::setThemeSearchPaths(QStringList() <setCurrentProfile(defaultProfile.isEmpty() ? ProjectManager::getDefaultProjectFormat() : defaultProfile); m_commandStack = new QUndoGroup(); // If using a custom profile, make sure the file exists or fallback to default QString currentProfilePath = pCore->getCurrentProfile()->path(); if (currentProfilePath.startsWith(QLatin1Char('/')) && !QFile::exists(currentProfilePath)) { KMessageBox::sorry(this, i18n("Cannot find your default profile, switching to ATSC 1080p 25")); pCore->setCurrentProfile(QStringLiteral("atsc_1080p_25")); KdenliveSettings::setDefault_profile(QStringLiteral("atsc_1080p_25")); } m_gpuAllowed = initEffects::parseEffectFiles(pCore->getMltRepository()); // initEffects::parseCustomEffectsFile(); m_shortcutRemoveFocus = new QShortcut(QKeySequence(QStringLiteral("Esc")), this); connect(m_shortcutRemoveFocus, &QShortcut::activated, this, &MainWindow::slotRemoveFocus); /// Add Widgets setDockOptions(dockOptions() | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) setDockOptions(dockOptions() | QMainWindow::GroupedDragging); #endif setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); m_timelineToolBarContainer = new QWidget(this); auto *ctnLay = new QVBoxLayout; ctnLay->setSpacing(0); ctnLay->setContentsMargins(0, 0, 0, 0); m_timelineToolBarContainer->setLayout(ctnLay); ctnLay->addWidget(m_timelineToolBar); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->applySettings(tbGroup); QFrame *fr = new QFrame(this); fr->setFrameShape(QFrame::HLine); fr->setMaximumHeight(1); fr->setLineWidth(1); ctnLay->addWidget(fr); setCentralWidget(m_timelineToolBarContainer); setupActions(); QDockWidget *libraryDock = addDock(i18n("Library"), QStringLiteral("library"), pCore->library()); m_clipMonitor = new Monitor(Kdenlive::ClipMonitor, pCore->monitorManager(), this); pCore->bin()->setMonitor(m_clipMonitor); connect(m_clipMonitor, &Monitor::showConfigDialog, this, &MainWindow::slotPreferences); connect(m_clipMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_clipMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteClipMarker); connect(m_clipMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_clipMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(pCore->bin(), &Bin::findInTimeline, this, &MainWindow::slotClipInTimeline); // TODO deprecated, replace with Bin methods if necessary /*connect(m_projectList, SIGNAL(loadingIsOver()), this, SLOT(slotElapsedTime())); connect(m_projectList, SIGNAL(updateRenderStatus()), this, SLOT(slotCheckRenderStatus())); connect(m_projectList, SIGNAL(updateProfile(QString)), this, SLOT(slotUpdateProjectProfile(QString))); connect(m_projectList, SIGNAL(refreshClip(QString,bool)), pCore->monitorManager(), SLOT(slotRefreshCurrentMonitor(QString))); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), m_projectList, SLOT(slotUpdateClipCut(QPoint)));*/ - connect(m_clipMonitor, &Monitor::extractZone, pCore->bin(), &Bin::slotStartCutJob); + + // TODO refac : reimplement ? + // connect(m_clipMonitor, &Monitor::extractZone, pCore->bin(), &Bin::slotStartCutJob); + connect(m_clipMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); m_projectMonitor = new Monitor(Kdenlive::ProjectMonitor, pCore->monitorManager(), this); connect(m_projectMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); connect(m_projectMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_projectMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteClipMarker); connect(m_projectMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_projectMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(m_loopClip, &QAction::triggered, m_projectMonitor, &Monitor::slotLoopClip); pCore->monitorManager()->initMonitors(m_clipMonitor, m_projectMonitor); connect(m_clipMonitor, &Monitor::addMasterEffect, pCore->bin(), &Bin::slotAddEffect); m_timelineTabs = new TimelineTabs(this); ctnLay->addWidget(m_timelineTabs); // Color schemes KActionMenu *themeAction = new KActionMenu(i18n("Theme"), this); ThemeManager::instance()->setThemeMenuAction(themeAction); ThemeManager::instance()->setCurrentTheme(KdenliveSettings::colortheme()); connect(ThemeManager::instance(), &ThemeManager::signalThemeChanged, this, &MainWindow::slotThemeChanged, Qt::DirectConnection); if (!KdenliveSettings::widgetstyle().isEmpty() && QString::compare(desktopStyle, KdenliveSettings::widgetstyle(), Qt::CaseInsensitive) != 0) { // User wants a custom widget style, init doChangeStyle(); } else { ThemeManager::instance()->slotChangePalette(); } // Audio spectrum scope m_audioSpectrum = new AudioGraphSpectrum(pCore->monitorManager()); QDockWidget *spectrumDock = addDock(i18n("Audio Spectrum"), QStringLiteral("audiospectrum"), m_audioSpectrum); connect(this, &MainWindow::reloadTheme, m_audioSpectrum, &AudioGraphSpectrum::refreshPixmap); // Close library and audiospectrum on first run libraryDock->close(); spectrumDock->close(); m_projectBinDock = addDock(i18n("Project Bin"), QStringLiteral("project_bin"), pCore->bin()); m_assetPanel = new AssetPanel(this); connect(m_assetPanel, &AssetPanel::doSplitEffect, m_projectMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::doSplitBinEffect, m_clipMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::changeSpeed, this, &MainWindow::slotChangeSpeed); connect(m_timelineTabs, &TimelineTabs::showTransitionModel, m_assetPanel, &AssetPanel::showTransition); connect(m_timelineTabs, &TimelineTabs::showItemEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(pCore->bin(), &Bin::requestShowEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(this, &MainWindow::clearAssetPanel, m_assetPanel, &AssetPanel::clearAssetPanel); connect(this, &MainWindow::adjustAssetPanelRange, m_assetPanel, &AssetPanel::adjustAssetPanelRange); - connect(m_assetPanel, &AssetPanel::seekToPos, [this](int pos){getCurrentTimeline()->controller()->setPosition(pos);} ); + connect(m_assetPanel, &AssetPanel::seekToPos, [this](int pos) { getCurrentTimeline()->controller()->setPosition(pos); }); m_effectStackDock = addDock(i18n("Properties"), QStringLiteral("effect_stack"), m_assetPanel); m_effectList = new EffectsListView(); // m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList); m_effectList2 = new EffectListWidget(this); connect(m_effectList2, &EffectListWidget::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList2); m_transitionList = new EffectsListView(EffectsListView::TransitionMode); m_transitionList2 = new TransitionListWidget(this); // m_transitionListDock = addDock(i18n("Transitions"), QStringLiteral("transition_list"), m_transitionList); m_transitionListDock = addDock(i18n("Transitions"), QStringLiteral("transition_list"), m_transitionList2); // Add monitors here to keep them at the right of the window m_clipMonitorDock = addDock(i18n("Clip Monitor"), QStringLiteral("clip_monitor"), m_clipMonitor); m_projectMonitorDock = addDock(i18n("Project Monitor"), QStringLiteral("project_monitor"), m_projectMonitor); m_undoView = new QUndoView(); m_undoView->setCleanIcon(KoIconUtils::themedIcon(QStringLiteral("edit-clear"))); m_undoView->setEmptyLabel(i18n("Clean")); m_undoView->setGroup(m_commandStack); m_undoViewDock = addDock(i18n("Undo History"), QStringLiteral("undo_history"), m_undoView); // Color and icon theme stuff addAction(QStringLiteral("themes_menu"), themeAction); connect(m_commandStack, &QUndoGroup::cleanChanged, m_saveAction, &QAction::setDisabled); addAction(QStringLiteral("styles_menu"), stylesAction); QAction *iconAction = new QAction(i18n("Force Breeze Icon Theme"), this); iconAction->setCheckable(true); iconAction->setChecked(KdenliveSettings::force_breeze()); addAction(QStringLiteral("force_icon_theme"), iconAction); connect(iconAction, &QAction::triggered, this, &MainWindow::forceIconSet); // Close non-general docks for the initial layout // only show important ones m_undoViewDock->close(); /// Tabify Widgets tabifyDockWidget(m_transitionListDock, m_effectListDock); tabifyDockWidget(m_effectStackDock, pCore->bin()->clipPropertiesDock()); // tabifyDockWidget(m_effectListDock, m_effectStackDock); tabifyDockWidget(m_clipMonitorDock, m_projectMonitorDock); bool firstRun = readOptions(); // Monitor Record action addAction(QStringLiteral("switch_monitor_rec"), m_clipMonitor->recAction()); // Build effects menu m_effectsMenu = new QMenu(i18n("Add Effect"), this); m_effectActions = new KActionCategory(i18n("Effects"), actionCollection()); m_effectList->reloadEffectList(m_effectsMenu, m_effectActions); m_transitionsMenu = new QMenu(i18n("Add Transition"), this); m_transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); m_transitionList->reloadEffectList(m_transitionsMenu, m_transitionActions); auto *scmanager = new ScopeManager(this); new LayoutManagement(this); new HideTitleBars(this); m_extraFactory = new KXMLGUIClient(this); buildDynamicActions(); // Create Effect Basket (dropdown list of favorites) m_effectBasket = new EffectBasket(m_effectList); connect(m_effectBasket, SIGNAL(addEffect(QDomElement)), this, SLOT(slotAddEffect(QDomElement))); auto *widgetlist = new QWidgetAction(this); widgetlist->setDefaultWidget(m_effectBasket); // widgetlist->setText(i18n("Favorite Effects")); widgetlist->setToolTip(i18n("Favorite Effects")); widgetlist->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); auto *menu = new QMenu(this); menu->addAction(widgetlist); auto *basketButton = new QToolButton(this); basketButton->setMenu(menu); basketButton->setToolButtonStyle(toolBar()->toolButtonStyle()); basketButton->setDefaultAction(widgetlist); basketButton->setPopupMode(QToolButton::InstantPopup); // basketButton->setText(i18n("Favorite Effects")); basketButton->setToolTip(i18n("Favorite Effects")); basketButton->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); auto *toolButtonAction = new QWidgetAction(this); toolButtonAction->setText(i18n("Favorite Effects")); toolButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); toolButtonAction->setDefaultWidget(basketButton); addAction(QStringLiteral("favorite_effects"), toolButtonAction); connect(toolButtonAction, &QAction::triggered, basketButton, &QToolButton::showMenu); // Render button ProgressButton *timelineRender = new ProgressButton(i18n("Render"), 100, this); auto *tlrMenu = new QMenu(this); timelineRender->setMenu(tlrMenu); connect(this, &MainWindow::setRenderProgress, timelineRender, &ProgressButton::setProgress); auto *renderButtonAction = new QWidgetAction(this); renderButtonAction->setText(i18n("Render Button")); renderButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("media-record"))); renderButtonAction->setDefaultWidget(timelineRender); addAction(QStringLiteral("project_render_button"), renderButtonAction); // Timeline preview button ProgressButton *timelinePreview = new ProgressButton(i18n("Rendering preview"), 1000, this); auto *tlMenu = new QMenu(this); timelinePreview->setMenu(tlMenu); connect(this, &MainWindow::setPreviewProgress, timelinePreview, &ProgressButton::setProgress); auto *previewButtonAction = new QWidgetAction(this); previewButtonAction->setText(i18n("Timeline Preview")); previewButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("preview-render-on"))); previewButtonAction->setDefaultWidget(timelinePreview); addAction(QStringLiteral("timeline_preview_button"), previewButtonAction); setupGUI(); if (firstRun) { QScreen *current = QApplication::primaryScreen(); if (current) { if (current->availableSize().height() < 1000) { resize(current->availableSize()); } else { resize(current->availableSize() / 1.5); } } } updateActionsToolTip(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); m_timelineToolBar->setProperty("otherToolbar", true); timelinePreview->setToolButtonStyle(m_timelineToolBar->toolButtonStyle()); connect(m_timelineToolBar, &QToolBar::toolButtonStyleChanged, timelinePreview, &ProgressButton::setToolButtonStyle); timelineRender->setToolButtonStyle(toolBar()->toolButtonStyle()); /*ScriptingPart* sp = new ScriptingPart(this, QStringList()); guiFactory()->addClient(sp);*/ loadGenerators(); loadDockActions(); loadClipActions(); // Connect monitor overlay info menu. QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); connect(monitorOverlay, &QMenu::triggered, this, &MainWindow::slotSwitchMonitorOverlay); m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); QMenu *clipInTimeline = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); clipInTimeline->setIcon(KoIconUtils::themedIcon(QStringLiteral("go-jump"))); pCore->bin()->setupGeneratorMenu(); connect(pCore->monitorManager(), &MonitorManager::updateOverlayInfos, this, &MainWindow::slotUpdateMonitorOverlays); // Setup and fill effects and transitions menus. QMenu *m = static_cast(factory()->container(QStringLiteral("video_effects_menu"), this)); connect(m, &QMenu::triggered, this, &MainWindow::slotAddVideoEffect); connect(m_effectsMenu, &QMenu::triggered, this, &MainWindow::slotAddVideoEffect); connect(m_transitionsMenu, &QMenu::triggered, this, &MainWindow::slotAddTransition); m_timelineContextMenu = new QMenu(this); m_timelineContextClipMenu = new QMenu(this); m_timelineContextTransitionMenu = new QMenu(this); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("insert_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space_all_tracks"))); m_timelineContextMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Paste))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("clip_in_project_tree"))); // m_timelineContextClipMenu->addAction(actionCollection()->action("clip_to_project_tree")); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("delete_timeline_clip"))); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("group_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("ungroup_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("split_audio"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("set_audio_align_ref"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("align_audio"))); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("cut_timeline_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Copy))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("paste_effects"))); m_timelineContextClipMenu->addSeparator(); QMenu *markersMenu = static_cast(factory()->container(QStringLiteral("marker_menu"), this)); m_timelineContextClipMenu->addMenu(markersMenu); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addMenu(m_transitionsMenu); m_timelineContextClipMenu->addMenu(m_effectsMenu); m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("delete_timeline_clip"))); m_timelineContextTransitionMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Copy))); m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("auto_transition"))); connect(m_effectList, &EffectsListView::addEffect, this, &MainWindow::slotAddEffect); connect(m_effectList, &EffectsListView::reloadEffects, this, &MainWindow::slotReloadEffects); slotConnectMonitors(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); // TODO: let user select timeline toolbar toolbutton style // connect(toolBar(), &QToolBar::iconSizeChanged, m_timelineToolBar, &QToolBar::setToolButtonStyle); m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); QAction *prevRender = actionCollection()->action(QStringLiteral("prerender_timeline_zone")); QAction *stopPrevRender = actionCollection()->action(QStringLiteral("stop_prerender_timeline")); tlMenu->addAction(stopPrevRender); tlMenu->addAction(actionCollection()->action(QStringLiteral("set_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("unset_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("clear_render_timeline_zone"))); // Automatic timeline preview action QAction *autoRender = new QAction(KoIconUtils::themedIcon(QStringLiteral("view-refresh")), i18n("Automatic Preview"), this); autoRender->setCheckable(true); autoRender->setChecked(KdenliveSettings::autopreview()); connect(autoRender, &QAction::triggered, this, &MainWindow::slotToggleAutoPreview); tlMenu->addAction(autoRender); tlMenu->addSeparator(); tlMenu->addAction(actionCollection()->action(QStringLiteral("disable_preview"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("manage_cache"))); timelinePreview->defineDefaultAction(prevRender, stopPrevRender); timelinePreview->setAutoRaise(true); QAction *showRender = actionCollection()->action(QStringLiteral("project_render")); tlrMenu->addAction(showRender); tlrMenu->addAction(actionCollection()->action(QStringLiteral("stop_project_render"))); timelineRender->defineDefaultAction(showRender, showRender); timelineRender->setAutoRaise(true); // Populate encoding profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); if (KdenliveSettings::proxyparams().isEmpty() || KdenliveSettings::proxyextension().isEmpty()) { KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString proxystring = i.value(); KdenliveSettings::setProxyparams(proxystring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setProxyextension(proxystring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::v4l_parameters().isEmpty() || KdenliveSettings::v4l_extension().isEmpty()) { KConfigGroup group(&conf, "video4linux"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString v4lstring = i.value(); KdenliveSettings::setV4l_parameters(v4lstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setV4l_extension(v4lstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::grab_parameters().isEmpty() || KdenliveSettings::grab_extension().isEmpty()) { KConfigGroup group(&conf, "screengrab"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString grabstring = i.value(); KdenliveSettings::setGrab_parameters(grabstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setGrab_extension(grabstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::decklink_parameters().isEmpty() || KdenliveSettings::decklink_extension().isEmpty()) { KConfigGroup group(&conf, "decklink"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString decklinkstring = i.value(); KdenliveSettings::setDecklink_parameters(decklinkstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setDecklink_extension(decklinkstring.section(QLatin1Char(';'), 1, 1)); } } if (!QDir(KdenliveSettings::currenttmpfolder()).isReadable()) KdenliveSettings::setCurrenttmpfolder(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); QTimer::singleShot(0, this, &MainWindow::GUISetupDone); connect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme, Qt::UniqueConnection); #ifdef USE_JOGSHUTTLE new JogManager(this); #endif scmanager->slotCheckActiveScopes(); // m_messageLabel->setMessage(QStringLiteral("This is a beta version. Always backup your data"), MltError); } void MainWindow::slotThemeChanged(const QString &theme) { disconnect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme); KSharedConfigPtr config = KSharedConfig::openConfig(theme); QPalette plt = KColorScheme::createApplicationPalette(config); setPalette(plt); qApp->setPalette(palette()); // Required for qml palette change QGuiApplication::setPalette(plt); KdenliveSettings::setColortheme(theme); QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (KdenliveSettings::force_breeze() && useDarkIcons != KdenliveSettings::use_dark_breeze()) { // We need to reload icon theme KdenliveSettings::setUse_dark_breeze(useDarkIcons); if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive needs to be restarted to apply color theme change. Restart now ?")) == KMessageBox::Continue) { slotRestart(); } } if (m_assetPanel) { m_assetPanel->updatePalette(); } if (m_effectList) { m_effectList->updatePalette(); } if (m_transitionList) { m_transitionList->updatePalette(); } if (m_clipMonitor) { m_clipMonitor->setPalette(plt); } if (m_projectMonitor) { m_projectMonitor->setPalette(plt); } m_timelineTabs->setPalette(plt); #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Not required anymore with auto colored icons since KF5 5.23 QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (m_themeInitialized && useDarkIcons != m_isDarkTheme) { if (pCore->bin()) { pCore->bin()->refreshIcons(); } if (m_clipMonitor) { m_clipMonitor->refreshIcons(); } if (m_projectMonitor) { m_projectMonitor->refreshIcons(); } if (pCore->monitorManager()) { pCore->monitorManager()->refreshIcons(); } if (m_effectList) { m_effectList->refreshIcons(); } for (QAction *action : actionCollection()->actions()) { QIcon icon = action->icon(); if (icon.isNull()) { continue; } QString iconName = icon.name(); QIcon newIcon = KoIconUtils::themedIcon(iconName); if (newIcon.isNull()) { continue; } action->setIcon(newIcon); } } m_themeInitialized = true; m_isDarkTheme = useDarkIcons; #endif connect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme, Qt::UniqueConnection); } bool MainWindow::event(QEvent *e) { switch (e->type()) { case QEvent::ApplicationPaletteChange: emit reloadTheme(); e->accept(); break; default: break; } return KXmlGuiWindow::event(e); } void MainWindow::updateActionsToolTip() { // Add shortcut to action tooltips QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { // find the shortcut pattern and delete (note the preceding space in the RegEx) QString strippedTooltip = tempAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (tempAction->shortcut() == QKeySequence(0)) { tempAction->setToolTip(strippedTooltip); } else { tempAction->setToolTip(strippedTooltip + QStringLiteral(" (") + tempAction->shortcut().toString() + QLatin1Char(')')); } connect(tempAction, &QAction::changed, this, &MainWindow::updateAction); } } } void MainWindow::updateAction() { QAction *action = qobject_cast(sender()); QString toolTip = KLocalizedString::removeAcceleratorMarker(action->toolTip()); QString strippedTooltip = toolTip.remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); action->setToolTip(i18nc("@info:tooltip Tooltip of toolbar button", "%1 (%2)", strippedTooltip, action->shortcut().toString())); } void MainWindow::slotReloadTheme() { ThemeManager::instance()->slotSettingsChanged(); } MainWindow::~MainWindow() { pCore->prepareShutdown(); delete m_audioSpectrum; if (m_projectMonitor) { m_projectMonitor->stop(); } if (m_clipMonitor) { m_clipMonitor->stop(); } ClipController::mediaUnavailable.reset(); delete m_projectMonitor; delete m_clipMonitor; delete m_shortcutRemoveFocus; delete m_effectList2; delete m_transitionList2; qDeleteAll(m_transitions); // Mlt::Factory::close(); } // virtual bool MainWindow::queryClose() { if (m_renderWidget) { int waitingJobs = m_renderWidget->waitingJobsCount(); if (waitingJobs > 0) { - switch (KMessageBox::warningYesNoCancel(this, i18np("You have 1 rendering job waiting in the queue.\nWhat do you want to do with this job?", - "You have %1 rendering jobs waiting in the queue.\nWhat do you want to do with these jobs?", - waitingJobs), - QString(), KGuiItem(i18n("Start them now")), KGuiItem(i18n("Delete them")))) { + switch ( + KMessageBox::warningYesNoCancel(this, + i18np("You have 1 rendering job waiting in the queue.\nWhat do you want to do with this job?", + "You have %1 rendering jobs waiting in the queue.\nWhat do you want to do with these jobs?", waitingJobs), + QString(), KGuiItem(i18n("Start them now")), KGuiItem(i18n("Delete them")))) { case KMessageBox::Yes: // create script with waiting jobs and start it if (!m_renderWidget->startWaitingRenderJobs()) { return false; } break; case KMessageBox::No: // Don't do anything, jobs will be deleted break; default: return false; } } } saveOptions(); // WARNING: According to KMainWindow::queryClose documentation we are not supposed to close the document here? return pCore->projectManager()->closeCurrentDocument(true, true); } void MainWindow::loadGenerators() { QMenu *addMenu = static_cast(factory()->container(QStringLiteral("generators"), this)); Generators::getGenerators(KdenliveSettings::producerslist(), addMenu); connect(addMenu, &QMenu::triggered, this, &MainWindow::buildGenerator); } void MainWindow::buildGenerator(QAction *action) { Generators gen(m_clipMonitor, action->data().toString(), this); if (gen.exec() == QDialog::Accepted) { pCore->bin()->slotAddClipToProject(gen.getSavedClip()); } } void MainWindow::saveProperties(KConfigGroup &config) { // save properties here KXmlGuiWindow::saveProperties(config); // TODO: fix session management if (qApp->isSavingSession() && pCore->projectManager()) { if (pCore->currentDoc() && !pCore->currentDoc()->url().isEmpty()) { config.writeEntry("kdenlive_lastUrl", pCore->currentDoc()->url().toLocalFile()); } } } void MainWindow::readProperties(const KConfigGroup &config) { // read properties here KXmlGuiWindow::readProperties(config); // TODO: fix session management /*if (qApp->isSessionRestored()) { pCore->projectManager()->openFile(QUrl::fromLocalFile(config.readEntry("kdenlive_lastUrl", QString()))); }*/ } void MainWindow::saveNewToolbarConfig() { KXmlGuiWindow::saveNewToolbarConfig(); // TODO for some reason all dynamically inserted actions are removed by the save toolbar // So we currently re-add them manually.... loadDockActions(); loadClipActions(); pCore->bin()->rebuildMenu(); QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (monitorOverlay) { m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); } } void MainWindow::slotReloadEffects() { initEffects::parseCustomEffectsFile(); m_effectList->reloadEffectList(m_effectsMenu, m_effectActions); } void MainWindow::configureNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::slotFullScreen() { KToggleFullScreenAction::setFullScreen(this, actionCollection()->action(QStringLiteral("fullscreen"))->isChecked()); } void MainWindow::slotAddEffect(const QDomElement &effect) { // TODO refac : reimplement /* if (effect.isNull()) { qCDebug(KDENLIVE_LOG) << "--- ERROR, TRYING TO APPEND nullptr EFFECT"; return; } QDomElement effectToAdd = effect.cloneNode().toElement(); EFFECTMODE status = m_effectStack->effectStatus(); switch (status) { case TIMELINE_TRACK: pCore->projectManager()->currentTimeline()->projectView()->slotAddTrackEffect(effectToAdd, m_effectStack->trackIndex()); break; case TIMELINE_CLIP: pCore->projectManager()->currentTimeline()->projectView()->slotAddEffectToCurrentItem(effectToAdd); break; case MASTER_CLIP: // TODO refac reimplement this. // pCore->bin()->slotEffectDropped(QString(), effectToAdd); break; default: // No clip selected m_messageLabel->setMessage(i18n("Select a clip if you want to apply an effect"), ErrorMessage); } */ } void MainWindow::slotConnectMonitors() { // connect(m_projectList, SIGNAL(deleteProjectClips(QStringList,QMap)), this, // SLOT(slotDeleteProjectClips(QStringList,QMap))); connect(m_clipMonitor, &Monitor::refreshClipThumbnail, pCore->bin(), &Bin::slotRefreshClipThumbnail); connect(m_projectMonitor, &Monitor::requestFrameForAnalysis, this, &MainWindow::slotMonitorRequestRenderFrame); connect(m_projectMonitor, &Monitor::createSplitOverlay, this, &MainWindow::createSplitOverlay); connect(m_projectMonitor, &Monitor::removeSplitOverlay, this, &MainWindow::removeSplitOverlay); } void MainWindow::createSplitOverlay(Mlt::Filter *filter) { getMainTimeline()->controller()->createSplitOverlay(filter); m_projectMonitor->activateSplit(); } void MainWindow::removeSplitOverlay() { getMainTimeline()->controller()->removeSplitOverlay(); } void MainWindow::addAction(const QString &name, QAction *action) { m_actionNames.append(name); actionCollection()->addAction(name, action); actionCollection()->setDefaultShortcut(action, action->shortcut()); // Fix warning about setDefaultShortcut } QAction *MainWindow::addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon, const QKeySequence &shortcut) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } if (!shortcut.isEmpty()) { action->setShortcut(shortcut); } addAction(name, action); connect(action, SIGNAL(triggered(bool)), receiver, member); return action; } void MainWindow::setupActions() { // create edit mode buttons m_normalEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-normal-edit")), i18n("Normal mode"), this); m_normalEditTool->setShortcut(i18nc("Normal editing", "n")); m_normalEditTool->setCheckable(true); m_normalEditTool->setChecked(true); m_overwriteEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-overwrite-edit")), i18n("Overwrite mode"), this); m_overwriteEditTool->setCheckable(true); m_overwriteEditTool->setChecked(false); m_insertEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-insert-edit")), i18n("Insert mode"), this); m_insertEditTool->setCheckable(true); m_insertEditTool->setChecked(false); KSelectAction *sceneMode = new KSelectAction(i18n("Timeline Edit Mode"), this); sceneMode->addAction(m_normalEditTool); sceneMode->addAction(m_overwriteEditTool); sceneMode->addAction(m_insertEditTool); sceneMode->setCurrentItem(0); connect(sceneMode, SIGNAL(triggered(QAction *)), this, SLOT(slotChangeEdit(QAction *))); addAction(QStringLiteral("timeline_mode"), sceneMode); KDualAction *ac = new KDualAction(i18n("Don't Use Timeline Zone for Insert"), i18n("Use Timeline Zone for Insert"), this); ac->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-on"))); ac->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-off"))); ac->setShortcut(Qt::Key_G); addAction(QStringLiteral("use_timeline_zone_in_edit"), ac); m_compositeAction = new KSelectAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-off")), i18n("Track compositing"), this); m_compositeAction->setToolTip(i18n("Track compositing")); QAction *noComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-off")), i18n("None"), this); noComposite->setCheckable(true); noComposite->setData(0); m_compositeAction->addAction(noComposite); QString compose = TransitionsRepository::get()->getCompositingTransition(); if (compose == QStringLiteral("movit.overlay")) { // Movit, do not show "preview" option since movit is faster QAction *hqComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setCheckable(true); hqComposite->setData(2); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { QAction *previewComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-preview")), i18n("Preview"), this); previewComposite->setCheckable(true); previewComposite->setData(1); m_compositeAction->addAction(previewComposite); if (compose != QStringLiteral("composite")) { QAction *hqComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setData(2); hqComposite->setCheckable(true); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { m_compositeAction->setCurrentAction(previewComposite); } } connect(m_compositeAction, SIGNAL(triggered(QAction *)), this, SLOT(slotUpdateCompositing(QAction *))); addAction(QStringLiteral("timeline_compositing"), m_compositeAction); m_timeFormatButton = new KSelectAction(QStringLiteral("00:00:00:00 / 00:00:00:00"), this); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_timeFormatButton->addAction(i18n("hh:mm:ss:ff")); m_timeFormatButton->addAction(i18n("Frames")); if (KdenliveSettings::frametimecode()) { m_timeFormatButton->setCurrentItem(1); } else { m_timeFormatButton->setCurrentItem(0); } connect(m_timeFormatButton, SIGNAL(triggered(int)), this, SLOT(slotUpdateTimecodeFormat(int))); m_timeFormatButton->setToolBarMode(KSelectAction::MenuMode); m_timeFormatButton->setToolButtonPopupMode(QToolButton::InstantPopup); addAction(QStringLiteral("timeline_timecode"), m_timeFormatButton); // create tools buttons m_buttonSelectTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("cursor-arrow")), i18n("Selection tool"), this); m_buttonSelectTool->setShortcut(i18nc("Selection tool shortcut", "s")); // toolbar->addAction(m_buttonSelectTool); m_buttonSelectTool->setCheckable(true); m_buttonSelectTool->setChecked(true); m_buttonRazorTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("edit-cut")), i18n("Razor tool"), this); m_buttonRazorTool->setShortcut(i18nc("Razor tool shortcut", "x")); // toolbar->addAction(m_buttonRazorTool); m_buttonRazorTool->setCheckable(true); m_buttonRazorTool->setChecked(false); m_buttonSpacerTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("distribute-horizontal-x")), i18n("Spacer tool"), this); m_buttonSpacerTool->setShortcut(i18nc("Spacer tool shortcut", "m")); // toolbar->addAction(m_buttonSpacerTool); m_buttonSpacerTool->setCheckable(true); m_buttonSpacerTool->setChecked(false); auto *toolGroup = new QActionGroup(this); toolGroup->addAction(m_buttonSelectTool); toolGroup->addAction(m_buttonRazorTool); toolGroup->addAction(m_buttonSpacerTool); toolGroup->setExclusive(true); // toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QWidget * actionWidget; int max = toolbar->iconSizeDefault() + 2; actionWidget = toolbar->widgetForAction(m_normalEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_insertEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_overwriteEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSelectTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonRazorTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSpacerTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4);*/ connect(toolGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeTool); // create automatic audio split button m_buttonAutomaticSplitAudio = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-split-audio")), i18n("Split audio and video automatically"), this); m_buttonAutomaticSplitAudio->setCheckable(true); m_buttonAutomaticSplitAudio->setChecked(KdenliveSettings::splitaudio()); connect(m_buttonAutomaticSplitAudio, &QAction::toggled, this, &MainWindow::slotSwitchSplitAudio); m_buttonVideoThumbs = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-videothumb")), i18n("Show video thumbnails"), this); m_buttonVideoThumbs->setCheckable(true); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); connect(m_buttonVideoThumbs, &QAction::triggered, this, &MainWindow::slotSwitchVideoThumbs); m_buttonAudioThumbs = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-audiothumb")), i18n("Show audio thumbnails"), this); m_buttonAudioThumbs->setCheckable(true); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); connect(m_buttonAudioThumbs, &QAction::triggered, this, &MainWindow::slotSwitchAudioThumbs); m_buttonShowMarkers = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-markers")), i18n("Show markers comments"), this); m_buttonShowMarkers->setCheckable(true); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); connect(m_buttonShowMarkers, &QAction::triggered, this, &MainWindow::slotSwitchMarkersComments); m_buttonSnap = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-snap")), i18n("Snap"), this); m_buttonSnap->setCheckable(true); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); connect(m_buttonSnap, &QAction::triggered, this, &MainWindow::slotSwitchSnap); m_buttonAutomaticTransition = new QAction(KoIconUtils::themedIcon(QStringLiteral("auto-transition")), i18n("Automatic transitions"), this); m_buttonAutomaticTransition->setCheckable(true); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); connect(m_buttonAutomaticTransition, &QAction::triggered, this, &MainWindow::slotSwitchAutomaticTransition); m_buttonFitZoom = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-fit-best")), i18n("Fit zoom to project"), this); m_buttonFitZoom->setCheckable(false); m_zoomOut = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-out")), i18n("Zoom Out"), this); m_zoomOut->setShortcut(Qt::CTRL + Qt::Key_Minus); m_zoomSlider = new QSlider(Qt::Horizontal, this); m_zoomSlider->setMaximum(13); m_zoomSlider->setPageStep(1); m_zoomSlider->setInvertedAppearance(true); m_zoomSlider->setInvertedControls(true); m_zoomSlider->setMaximumWidth(150); m_zoomSlider->setMinimumWidth(100); m_zoomIn = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-in")), i18n("Zoom In"), this); m_zoomIn->setShortcut(Qt::CTRL + Qt::Key_Plus); /*actionWidget = toolbar->widgetForAction(m_buttonFitZoom); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless); actionWidget = toolbar->widgetForAction(m_zoomIn); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless); actionWidget = toolbar->widgetForAction(m_zoomOut); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless);*/ connect(m_zoomSlider, SIGNAL(valueChanged(int)), this, SLOT(slotSetZoom(int))); connect(m_zoomSlider, &QAbstractSlider::sliderMoved, this, &MainWindow::slotShowZoomSliderToolTip); connect(m_buttonFitZoom, &QAction::triggered, this, &MainWindow::slotFitZoom); connect(m_zoomIn, &QAction::triggered, this, &MainWindow::slotZoomIn); connect(m_zoomOut, &QAction::triggered, this, &MainWindow::slotZoomOut); m_trimLabel = new QLabel(QStringLiteral(" "), this); m_trimLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // m_trimLabel->setAutoFillBackground(true); m_trimLabel->setAlignment(Qt::AlignHCenter); m_trimLabel->setStyleSheet(QStringLiteral("QLabel { background-color :red; }")); KToolBar *toolbar = new KToolBar(QStringLiteral("statusToolBar"), this, Qt::BottomToolBarArea); toolbar->setMovable(false); toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QString styleBorderless = QStringLiteral("QToolButton { border-width: 0px;margin: 1px 3px 0px;padding: 0px;}");*/ toolbar->addWidget(m_trimLabel); toolbar->addAction(m_buttonAutomaticSplitAudio); toolbar->addAction(m_buttonAutomaticTransition); toolbar->addAction(m_buttonVideoThumbs); toolbar->addAction(m_buttonAudioThumbs); toolbar->addAction(m_buttonShowMarkers); toolbar->addAction(m_buttonSnap); toolbar->addSeparator(); toolbar->addAction(m_buttonFitZoom); toolbar->addAction(m_zoomOut); toolbar->addWidget(m_zoomSlider); toolbar->addAction(m_zoomIn); /*actionWidget = toolbar->widgetForAction(m_buttonAutomaticSplitAudio); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonVideoThumbs); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonAudioThumbs); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonShowMarkers); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSnap); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4);*/ int small = style()->pixelMetric(QStyle::PM_SmallIconSize); statusBar()->setMaximumHeight(2 * small); m_messageLabel = new StatusBarMessageLabel(this); m_messageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding); connect(this, &MainWindow::displayMessage, m_messageLabel, &StatusBarMessageLabel::setMessage); statusBar()->addWidget(m_messageLabel, 0); QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); statusBar()->addWidget(spacer, 1); statusBar()->addPermanentWidget(toolbar); toolbar->setIconSize(QSize(small, small)); toolbar->layout()->setContentsMargins(0, 0, 0, 0); statusBar()->setContentsMargins(0, 0, 0, 0); addAction(QStringLiteral("normal_mode"), m_normalEditTool); addAction(QStringLiteral("overwrite_mode"), m_overwriteEditTool); addAction(QStringLiteral("insert_mode"), m_insertEditTool); addAction(QStringLiteral("select_tool"), m_buttonSelectTool); addAction(QStringLiteral("razor_tool"), m_buttonRazorTool); addAction(QStringLiteral("spacer_tool"), m_buttonSpacerTool); addAction(QStringLiteral("automatic_split_audio"), m_buttonAutomaticSplitAudio); addAction(QStringLiteral("automatic_transition"), m_buttonAutomaticTransition); addAction(QStringLiteral("show_video_thumbs"), m_buttonVideoThumbs); addAction(QStringLiteral("show_audio_thumbs"), m_buttonAudioThumbs); addAction(QStringLiteral("show_markers"), m_buttonShowMarkers); addAction(QStringLiteral("snap"), m_buttonSnap); addAction(QStringLiteral("zoom_fit"), m_buttonFitZoom); addAction(QStringLiteral("zoom_in"), m_zoomIn); addAction(QStringLiteral("zoom_out"), m_zoomOut); KNS3::standardAction(i18n("Download New Wipes..."), this, SLOT(slotGetNewLumaStuff()), actionCollection(), "get_new_lumas"); KNS3::standardAction(i18n("Download New Render Profiles..."), this, SLOT(slotGetNewRenderStuff()), actionCollection(), "get_new_profiles"); KNS3::standardAction(i18n("Download New Title Templates..."), this, SLOT(slotGetNewTitleStuff()), actionCollection(), "get_new_titles"); addAction(QStringLiteral("run_wizard"), i18n("Run Config Wizard"), this, SLOT(slotRunWizard()), KoIconUtils::themedIcon(QStringLiteral("tools-wizard"))); addAction(QStringLiteral("project_settings"), i18n("Project Settings"), this, SLOT(slotEditProjectSettings()), KoIconUtils::themedIcon(QStringLiteral("configure"))); addAction(QStringLiteral("project_render"), i18n("Render"), this, SLOT(slotRenderProject()), KoIconUtils::themedIcon(QStringLiteral("media-record")), Qt::CTRL + Qt::Key_Return); addAction(QStringLiteral("stop_project_render"), i18n("Stop Render"), this, SLOT(slotStopRenderProject()), KoIconUtils::themedIcon(QStringLiteral("media-record"))); addAction(QStringLiteral("project_clean"), i18n("Clean Project"), this, SLOT(slotCleanProject()), KoIconUtils::themedIcon(QStringLiteral("edit-clear"))); // TODO // addAction("project_adjust_profile", i18n("Adjust Profile to Current Clip"), pCore->bin(), SLOT(adjustProjectProfileToItem())); m_playZone = addAction(QStringLiteral("monitor_play_zone"), i18n("Play Zone"), pCore->monitorManager(), SLOT(slotPlayZone()), KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), Qt::CTRL + Qt::Key_Space); m_loopZone = addAction(QStringLiteral("monitor_loop_zone"), i18n("Loop Zone"), pCore->monitorManager(), SLOT(slotLoopZone()), KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), Qt::ALT + Qt::Key_Space); m_loopClip = new QAction(KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), i18n("Loop selected clip"), this); addAction(QStringLiteral("monitor_loop_clip"), m_loopClip); m_loopClip->setEnabled(false); addAction(QStringLiteral("dvd_wizard"), i18n("DVD Wizard"), this, SLOT(slotDvdWizard()), KoIconUtils::themedIcon(QStringLiteral("media-optical"))); addAction(QStringLiteral("transcode_clip"), i18n("Transcode Clips"), this, SLOT(slotTranscodeClip()), KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); addAction(QStringLiteral("archive_project"), i18n("Archive Project"), this, SLOT(slotArchiveProject()), KoIconUtils::themedIcon(QStringLiteral("document-save-all"))); addAction(QStringLiteral("switch_monitor"), i18n("Switch monitor"), this, SLOT(slotSwitchMonitors()), QIcon(), Qt::Key_T); addAction(QStringLiteral("expand_timeline_clip"), i18n("Expand Clip"), pCore->projectManager(), SLOT(slotExpandClip()), KoIconUtils::themedIcon(QStringLiteral("document-open"))); QAction *overlayInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Info Overlay"), this); addAction(QStringLiteral("monitor_overlay"), overlayInfo); overlayInfo->setCheckable(true); overlayInfo->setData(0x01); QAction *overlayTCInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Timecode"), this); addAction(QStringLiteral("monitor_overlay_tc"), overlayTCInfo); overlayTCInfo->setCheckable(true); overlayTCInfo->setData(0x02); QAction *overlayFpsInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Playback Fps"), this); addAction(QStringLiteral("monitor_overlay_fps"), overlayFpsInfo); overlayFpsInfo->setCheckable(true); overlayFpsInfo->setData(0x20); QAction *overlayMarkerInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Markers"), this); addAction(QStringLiteral("monitor_overlay_markers"), overlayMarkerInfo); overlayMarkerInfo->setCheckable(true); overlayMarkerInfo->setData(0x04); QAction *overlaySafeInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Safe Zones"), this); addAction(QStringLiteral("monitor_overlay_safezone"), overlaySafeInfo); overlaySafeInfo->setCheckable(true); overlaySafeInfo->setData(0x08); QAction *overlayAudioInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Audio Waveform"), this); addAction(QStringLiteral("monitor_overlay_audiothumb"), overlayAudioInfo); overlayAudioInfo->setCheckable(true); overlayAudioInfo->setData(0x10); QAction *dropFrames = new QAction(QIcon(), i18n("Real Time (drop frames)"), this); dropFrames->setCheckable(true); dropFrames->setChecked(KdenliveSettings::monitor_dropframes()); connect(dropFrames, &QAction::toggled, this, &MainWindow::slotSwitchDropFrames); KSelectAction *monitorGamma = new KSelectAction(i18n("Monitor Gamma"), this); monitorGamma->addAction(i18n("sRGB (computer)")); monitorGamma->addAction(i18n("Rec. 709 (TV)")); addAction(QStringLiteral("mlt_gamma"), monitorGamma); monitorGamma->setCurrentItem(KdenliveSettings::monitor_gamma()); connect(monitorGamma, SIGNAL(triggered(int)), this, SLOT(slotSetMonitorGamma(int))); addAction(QStringLiteral("switch_trim"), i18n("Trim Mode"), this, SLOT(slotSwitchTrimMode()), KoIconUtils::themedIcon(QStringLiteral("cursor-arrow"))); // disable shortcut until fully working, Qt::CTRL + Qt::Key_T); addAction(QStringLiteral("insert_project_tree"), i18n("Insert Zone in Project Bin"), this, SLOT(slotInsertZoneToTree()), QIcon(), Qt::CTRL + Qt::Key_I); addAction(QStringLiteral("insert_timeline"), i18n("Insert Zone in Timeline"), this, SLOT(slotInsertZoneToTimeline()), QIcon(), Qt::SHIFT + Qt::CTRL + Qt::Key_I); QAction *resizeStart = new QAction(QIcon(), i18n("Resize Item Start"), this); addAction(QStringLiteral("resize_timeline_clip_start"), resizeStart); resizeStart->setShortcut(Qt::Key_1); connect(resizeStart, &QAction::triggered, this, &MainWindow::slotResizeItemStart); QAction *resizeEnd = new QAction(QIcon(), i18n("Resize Item End"), this); addAction(QStringLiteral("resize_timeline_clip_end"), resizeEnd); resizeEnd->setShortcut(Qt::Key_2); connect(resizeEnd, &QAction::triggered, this, &MainWindow::slotResizeItemEnd); addAction(QStringLiteral("monitor_seek_snap_backward"), i18n("Go to Previous Snap Point"), this, SLOT(slotSnapRewind()), KoIconUtils::themedIcon(QStringLiteral("media-seek-backward")), Qt::ALT + Qt::Key_Left); addAction(QStringLiteral("seek_clip_start"), i18n("Go to Clip Start"), this, SLOT(slotClipStart()), KoIconUtils::themedIcon(QStringLiteral("media-seek-backward")), Qt::Key_Home); addAction(QStringLiteral("seek_clip_end"), i18n("Go to Clip End"), this, SLOT(slotClipEnd()), KoIconUtils::themedIcon(QStringLiteral("media-seek-forward")), Qt::Key_End); addAction(QStringLiteral("monitor_seek_snap_forward"), i18n("Go to Next Snap Point"), this, SLOT(slotSnapForward()), KoIconUtils::themedIcon(QStringLiteral("media-seek-forward")), Qt::ALT + Qt::Key_Right); addAction(QStringLiteral("delete_timeline_clip"), i18n("Delete Selected Item"), this, SLOT(slotDeleteItem()), KoIconUtils::themedIcon(QStringLiteral("edit-delete")), Qt::Key_Delete); addAction(QStringLiteral("align_playhead"), i18n("Align Playhead to Mouse Position"), this, SLOT(slotAlignPlayheadToMousePos()), QIcon(), Qt::Key_P); QAction *stickTransition = new QAction(i18n("Automatic Transition"), this); stickTransition->setData(QStringLiteral("auto")); stickTransition->setCheckable(true); stickTransition->setEnabled(false); addAction(QStringLiteral("auto_transition"), stickTransition); connect(stickTransition, &QAction::triggered, this, &MainWindow::slotAutoTransition); addAction(QStringLiteral("group_clip"), i18n("Group Clips"), this, SLOT(slotGroupClips()), KoIconUtils::themedIcon(QStringLiteral("object-group")), Qt::CTRL + Qt::Key_G); QAction *ungroupClip = addAction(QStringLiteral("ungroup_clip"), i18n("Ungroup Clips"), this, SLOT(slotUnGroupClips()), KoIconUtils::themedIcon(QStringLiteral("object-ungroup")), Qt::CTRL + Qt::SHIFT + Qt::Key_G); ungroupClip->setData("ungroup_clip"); addAction(QStringLiteral("edit_item_duration"), i18n("Edit Duration"), this, SLOT(slotEditItemDuration()), KoIconUtils::themedIcon(QStringLiteral("measure"))); addAction(QStringLiteral("clip_in_project_tree"), i18n("Clip in Project Bin"), this, SLOT(slotClipInProjectTree()), KoIconUtils::themedIcon(QStringLiteral("go-jump-definition"))); addAction(QStringLiteral("overwrite_to_in_point"), i18n("Overwrite Clip Zone in Timeline"), this, SLOT(slotInsertClipOverwrite()), KoIconUtils::themedIcon(QStringLiteral("timeline-overwrite")), Qt::Key_B); addAction(QStringLiteral("insert_to_in_point"), i18n("Insert Clip Zone in Timeline"), this, SLOT(slotInsertClipInsert()), KoIconUtils::themedIcon(QStringLiteral("timeline-insert")), Qt::Key_V); addAction(QStringLiteral("remove_extract"), i18n("Extract Timeline Zone"), this, SLOT(slotExtractZone()), KoIconUtils::themedIcon(QStringLiteral("timeline-extract")), Qt::SHIFT + Qt::Key_X); addAction(QStringLiteral("remove_lift"), i18n("Lift Timeline Zone"), this, SLOT(slotLiftZone()), KoIconUtils::themedIcon(QStringLiteral("timeline-lift")), Qt::Key_Z); addAction(QStringLiteral("set_render_timeline_zone"), i18n("Add Preview Zone"), this, SLOT(slotDefinePreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-add-zone"))); addAction(QStringLiteral("unset_render_timeline_zone"), i18n("Remove Preview Zone"), this, SLOT(slotRemovePreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-remove-zone"))); addAction(QStringLiteral("clear_render_timeline_zone"), i18n("Remove All Preview Zones"), this, SLOT(slotClearPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-remove-all"))); addAction(QStringLiteral("prerender_timeline_zone"), i18n("Start Preview Render"), this, SLOT(slotPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-render-on")), QKeySequence(Qt::SHIFT + Qt::Key_Return)); addAction(QStringLiteral("stop_prerender_timeline"), i18n("Stop Preview Render"), this, SLOT(slotStopPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-render-off"))); addAction(QStringLiteral("select_timeline_clip"), i18n("Select Clip"), this, SLOT(slotSelectTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_clip"), i18n("Deselect Clip"), this, SLOT(slotDeselectTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_clip"), i18n("Add Clip To Selection"), this, SLOT(slotSelectAddTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::ALT + Qt::Key_Plus); addAction(QStringLiteral("select_timeline_transition"), i18n("Select Transition"), this, SLOT(slotSelectTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_transition"), i18n("Deselect Transition"), this, SLOT(slotDeselectTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_transition"), i18n("Add Transition To Selection"), this, SLOT(slotSelectAddTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::ALT + Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("cut_timeline_clip"), i18n("Cut Clip"), this, SLOT(slotCutTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-cut")), Qt::SHIFT + Qt::Key_R); addAction(QStringLiteral("add_clip_marker"), i18n("Add Marker"), this, SLOT(slotAddClipMarker()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new"))); addAction(QStringLiteral("delete_clip_marker"), i18n("Delete Marker"), this, SLOT(slotDeleteClipMarker()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); addAction(QStringLiteral("delete_all_clip_markers"), i18n("Delete All Markers"), this, SLOT(slotDeleteAllClipMarkers()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); QAction *editClipMarker = addAction(QStringLiteral("edit_clip_marker"), i18n("Edit Marker"), this, SLOT(slotEditClipMarker()), KoIconUtils::themedIcon(QStringLiteral("document-properties"))); editClipMarker->setData(QStringLiteral("edit_marker")); addAction(QStringLiteral("add_marker_guide_quickly"), i18n("Add Marker/Guide quickly"), this, SLOT(slotAddMarkerGuideQuickly()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new")), Qt::Key_Asterisk); QAction *splitAudio = addAction(QStringLiteral("split_audio"), i18n("Split Audio"), this, SLOT(slotSplitAudio()), KoIconUtils::themedIcon(QStringLiteral("document-new"))); // "A+V" as data means this action should only be available for clips with audio AND video splitAudio->setData("A+V"); QAction *setAudioAlignReference = addAction(QStringLiteral("set_audio_align_ref"), i18n("Set Audio Reference"), this, SLOT(slotSetAudioAlignReference())); // "A" as data means this action should only be available for clips with audio setAudioAlignReference->setData("A"); QAction *alignAudio = addAction(QStringLiteral("align_audio"), i18n("Align Audio to Reference"), this, SLOT(slotAlignAudio()), QIcon()); // "A" as data means this action should only be available for clips with audio alignAudio->setData("A"); QAction *audioOnly = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Audio Only"), this); addAction(QStringLiteral("clip_audio_only"), audioOnly); audioOnly->setData(PlaylistState::AudioOnly); audioOnly->setCheckable(true); QAction *videoOnly = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Video Only"), this); addAction(QStringLiteral("clip_video_only"), videoOnly); videoOnly->setData(PlaylistState::VideoOnly); videoOnly->setCheckable(true); QAction *audioAndVideo = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Audio and Video"), this); addAction(QStringLiteral("clip_audio_and_video"), audioAndVideo); audioAndVideo->setData(PlaylistState::Original); audioAndVideo->setCheckable(true); m_clipTypeGroup = new QActionGroup(this); m_clipTypeGroup->addAction(audioOnly); m_clipTypeGroup->addAction(videoOnly); m_clipTypeGroup->addAction(audioAndVideo); connect(m_clipTypeGroup, &QActionGroup::triggered, this, &MainWindow::slotUpdateClipType); m_clipTypeGroup->setEnabled(false); addAction(QStringLiteral("insert_space"), i18n("Insert Space"), this, SLOT(slotInsertSpace())); addAction(QStringLiteral("delete_space"), i18n("Remove Space"), this, SLOT(slotRemoveSpace())); addAction(QStringLiteral("delete_space_all_tracks"), i18n("Remove Space In All Tracks"), this, SLOT(slotRemoveAllSpace())); KActionCategory *timelineActions = new KActionCategory(i18n("Tracks"), actionCollection()); QAction *insertTrack = new QAction(QIcon(), i18n("Insert Track"), this); connect(insertTrack, &QAction::triggered, this, &MainWindow::slotInsertTrack); timelineActions->addAction(QStringLiteral("insert_track"), insertTrack); QAction *deleteTrack = new QAction(QIcon(), i18n("Delete Track"), this); connect(deleteTrack, &QAction::triggered, this, &MainWindow::slotDeleteTrack); timelineActions->addAction(QStringLiteral("delete_track"), deleteTrack); deleteTrack->setData("delete_track"); QAction *configTracks = new QAction(KoIconUtils::themedIcon(QStringLiteral("configure")), i18n("Configure Tracks"), this); connect(configTracks, &QAction::triggered, this, &MainWindow::slotConfigTrack); timelineActions->addAction(QStringLiteral("config_tracks"), configTracks); QAction *selectTrack = new QAction(QIcon(), i18n("Select All in Current Track"), this); connect(selectTrack, &QAction::triggered, this, &MainWindow::slotSelectTrack); timelineActions->addAction(QStringLiteral("select_track"), selectTrack); QAction *selectAll = KStandardAction::selectAll(this, SLOT(slotSelectAllTracks()), this); selectAll->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-select-all"))); selectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("select_all_tracks"), selectAll); QAction *unselectAll = KStandardAction::deselect(this, SLOT(slotUnselectAllTracks()), this); unselectAll->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-unselect-all"))); unselectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("unselect_all_tracks"), unselectAll); kdenliveCategoryMap.insert(QStringLiteral("timeline"), timelineActions); // Cached data management addAction(QStringLiteral("manage_cache"), i18n("Manage Cached Data"), this, SLOT(slotManageCache()), KoIconUtils::themedIcon(QStringLiteral("network-server-database"))); QAction *disablePreview = new QAction(i18n("Disable Timeline Preview"), this); disablePreview->setCheckable(true); addAction(QStringLiteral("disable_preview"), disablePreview); addAction(QStringLiteral("add_guide"), i18n("Add Guide"), this, SLOT(slotAddGuide()), KoIconUtils::themedIcon(QStringLiteral("list-add"))); addAction(QStringLiteral("delete_guide"), i18n("Delete Guide"), this, SLOT(slotDeleteGuide()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); addAction(QStringLiteral("edit_guide"), i18n("Edit Guide"), this, SLOT(slotEditGuide()), KoIconUtils::themedIcon(QStringLiteral("document-properties"))); addAction(QStringLiteral("delete_all_guides"), i18n("Delete All Guides"), this, SLOT(slotDeleteAllGuides()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); QAction *pasteEffects = addAction(QStringLiteral("paste_effects"), i18n("Paste Effects"), this, SLOT(slotPasteEffects()), KoIconUtils::themedIcon(QStringLiteral("edit-paste"))); pasteEffects->setData("paste_effects"); m_saveAction = KStandardAction::save(pCore->projectManager(), SLOT(saveFile()), actionCollection()); m_saveAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-save"))); addAction(QStringLiteral("save_selection"), i18n("Save Selection"), pCore->projectManager(), SLOT(slotSaveSelection()), KoIconUtils::themedIcon(QStringLiteral("document-save"))); QAction *sentToLibrary = addAction(QStringLiteral("send_library"), i18n("Add Timeline Selection to Library"), pCore->library(), SLOT(slotAddToLibrary()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new"))); pCore->library()->setupActions(QList() << sentToLibrary); KStandardAction::showMenubar(this, SLOT(showMenuBar(bool)), actionCollection()); QAction *a = KStandardAction::quit(this, SLOT(close()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("application-exit"))); // TODO: make the following connection to slotEditKeys work // KStandardAction::keyBindings(this, SLOT(slotEditKeys()), actionCollection()); a = KStandardAction::preferences(this, SLOT(slotPreferences()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); a = KStandardAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); a = KStandardAction::copy(this, SLOT(slotCopy()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); a = KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-paste"))); a = KStandardAction::fullScreen(this, SLOT(slotFullScreen()), this, actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("view-fullscreen"))); QAction *undo = KStandardAction::undo(m_commandStack, SLOT(undo()), actionCollection()); undo->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-undo"))); undo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canUndoChanged, undo, &QAction::setEnabled); QAction *redo = KStandardAction::redo(m_commandStack, SLOT(redo()), actionCollection()); redo->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-redo"))); redo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canRedoChanged, redo, &QAction::setEnabled); auto *addClips = new QMenu(this); QAction *addClip = addAction(QStringLiteral("add_clip"), i18n("Add Clip"), pCore->bin(), SLOT(slotAddClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-clip"))); addClips->addAction(addClip); QAction *action = addAction(QStringLiteral("add_color_clip"), i18n("Add Color Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-color-clip"))); - action->setData((int)Color); + action->setData((int)ClipType::Color); addClips->addAction(action); action = addAction(QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-slide-clip"))); - action->setData((int)SlideShow); + action->setData((int)ClipType::SlideShow); addClips->addAction(action); action = addAction(QStringLiteral("add_text_clip"), i18n("Add Title Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); - action->setData((int)Text); + action->setData((int)ClipType::Text); addClips->addAction(action); action = addAction(QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); - action->setData((int)TextTemplate); + action->setData((int)ClipType::TextTemplate); addClips->addAction(action); /*action = addAction(QStringLiteral("add_qtext_clip"), i18n("Add Simple Text Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int) QText); addClips->addAction(action);*/ QAction *addFolder = addAction(QStringLiteral("add_folder"), i18n("Create Folder"), pCore->bin(), SLOT(slotAddFolder()), KoIconUtils::themedIcon(QStringLiteral("folder-new"))); addClips->addAction(addAction(QStringLiteral("download_resource"), i18n("Online Resources"), this, SLOT(slotDownloadResources()), KoIconUtils::themedIcon(QStringLiteral("edit-download")))); QAction *clipProperties = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), pCore->bin(), SLOT(slotSwitchClipProperties()), KoIconUtils::themedIcon(QStringLiteral("document-edit"))); clipProperties->setData("clip_properties"); QAction *openClip = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), pCore->bin(), SLOT(slotOpenClip()), KoIconUtils::themedIcon(QStringLiteral("document-open"))); openClip->setData("edit_clip"); openClip->setEnabled(false); QAction *deleteClip = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), pCore->bin(), SLOT(slotDeleteClip()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); deleteClip->setData("delete_clip"); deleteClip->setEnabled(false); QAction *reloadClip = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), pCore->bin(), SLOT(slotReloadClip()), KoIconUtils::themedIcon(QStringLiteral("view-refresh"))); reloadClip->setData("reload_clip"); reloadClip->setEnabled(false); QAction *disableEffects = addAction(QStringLiteral("disable_timeline_effects"), i18n("Disable Timeline Effects"), pCore->projectManager(), SLOT(slotDisableTimelineEffects(bool)), KoIconUtils::themedIcon(QStringLiteral("favorite"))); disableEffects->setData("disable_timeline_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); QAction *locateClip = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip..."), pCore->bin(), SLOT(slotLocateClip()), KoIconUtils::themedIcon(QStringLiteral("edit-file"))); locateClip->setData("locate_clip"); locateClip->setEnabled(false); QAction *duplicateClip = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), pCore->bin(), SLOT(slotDuplicateClip()), KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); duplicateClip->setData("duplicate_clip"); duplicateClip->setEnabled(false); QAction *proxyClip = new QAction(i18n("Proxy Clip"), this); addAction(QStringLiteral("proxy_clip"), proxyClip); proxyClip->setData(QStringList() << QString::number((int)AbstractClipJob::PROXYJOB)); proxyClip->setCheckable(true); proxyClip->setChecked(false); addAction(QStringLiteral("switch_track_lock"), i18n("Toggle Track Lock"), pCore->projectManager(), SLOT(slotSwitchTrackLock()), QIcon(), Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_all_track_lock"), i18n("Toggle All Track Lock"), pCore->projectManager(), SLOT(slotSwitchAllTrackLock()), QIcon(), Qt::CTRL + Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_track_target"), i18n("Toggle Track Target"), pCore->projectManager(), SLOT(slotSwitchTrackTarget()), QIcon(), Qt::SHIFT + Qt::Key_T); QHash actions; actions.insert(QStringLiteral("locate"), locateClip); actions.insert(QStringLiteral("reload"), reloadClip); actions.insert(QStringLiteral("duplicate"), duplicateClip); actions.insert(QStringLiteral("proxy"), proxyClip); actions.insert(QStringLiteral("properties"), clipProperties); actions.insert(QStringLiteral("open"), openClip); actions.insert(QStringLiteral("delete"), deleteClip); actions.insert(QStringLiteral("folder"), addFolder); pCore->bin()->setupMenu(addClips, addClip, actions); // Setup effects and transitions actions. KActionCategory *transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); // m_transitions = new QAction*[transitions.count()]; for (int i = 0; i < transitions.count(); ++i) { QStringList effectInfo = transitions.effectIdInfo(i); if (effectInfo.isEmpty()) { continue; } auto *transAction = new QAction(effectInfo.at(0), this); transAction->setData(effectInfo); transAction->setIconVisibleInMenu(false); m_transitions << transAction; QString id = effectInfo.at(2); if (id.isEmpty()) { id = effectInfo.at(1); } transitionActions->addAction("transition_" + id, transAction); } // monitor actions - addAction(QStringLiteral("extract_frame"), i18n("Extract frame..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrame()), KoIconUtils::themedIcon(QStringLiteral("insert-image"))); + addAction(QStringLiteral("extract_frame"), i18n("Extract frame..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrame()), + KoIconUtils::themedIcon(QStringLiteral("insert-image"))); - addAction(QStringLiteral("extract_frame_to_project"), i18n("Extract frame to project..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrameToProject()), KoIconUtils::themedIcon(QStringLiteral("insert-image"))); + addAction(QStringLiteral("extract_frame_to_project"), i18n("Extract frame to project..."), pCore->monitorManager(), + SLOT(slotExtractCurrentFrameToProject()), KoIconUtils::themedIcon(QStringLiteral("insert-image"))); } void MainWindow::saveOptions() { KdenliveSettings::self()->save(); } bool MainWindow::readOptions() { KSharedConfigPtr config = KSharedConfig::openConfig(); pCore->projectManager()->recentFilesAction()->loadEntries(KConfigGroup(config, "Recent Files")); if (KdenliveSettings::defaultprojectfolder().isEmpty()) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); dir.mkpath(QStringLiteral(".")); KdenliveSettings::setDefaultprojectfolder(dir.absolutePath()); } if (KdenliveSettings::trackheight() == 0) { QFontMetrics metrics(font()); int trackHeight = 2 * metrics.height(); QStyle *style = qApp->style(); trackHeight += style->pixelMetric(QStyle::PM_ToolBarIconSize) + 2 * style->pixelMetric(QStyle::PM_ToolBarItemMargin) + style->pixelMetric(QStyle::PM_ToolBarItemSpacing) + 2; KdenliveSettings::setTrackheight(trackHeight); } if (KdenliveSettings::trackheight() == 0) { KdenliveSettings::setTrackheight(50); } bool firstRun = false; KConfigGroup initialGroup(config, "version"); if (!initialGroup.exists()) { // First run, check if user is on a KDE Desktop firstRun = true; // this is our first run, show Wizard QPointer w = new Wizard(true); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); delete w; } else { delete w; ::exit(1); } } else if (!KdenliveSettings::ffmpegpath().isEmpty() && !QFile::exists(KdenliveSettings::ffmpegpath())) { // Invalid entry for FFmpeg, check system QPointer w = new Wizard(true); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } initialGroup.writeEntry("version", version); return firstRun; } void MainWindow::slotRunWizard() { QPointer w = new Wizard(false, this); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } void MainWindow::slotRefreshProfiles() { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (d) { d->checkProfile(); } } void MainWindow::slotEditProjectSettings() { KdenliveDoc *project = pCore->currentDoc(); QPoint p = getMainTimeline()->getTracksCount(); - ProjectSettings *w = new ProjectSettings(project, project->metadata(), getMainTimeline()->controller()->extractCompositionLumas(), p.x(), - p.y(), project->projectTempFolder(), true, !project->isModified(), this); + ProjectSettings *w = new ProjectSettings(project, project->metadata(), getMainTimeline()->controller()->extractCompositionLumas(), p.x(), p.y(), + project->projectTempFolder(), true, !project->isModified(), this); connect(w, &ProjectSettings::disableProxies, this, &MainWindow::slotDisableProxies); - //connect(w, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); + // connect(w, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); connect(w, &ProjectSettings::refreshProfiles, this, &MainWindow::slotRefreshProfiles); if (w->exec() == QDialog::Accepted) { QString profile = w->selectedProfile(); // project->setProjectFolder(w->selectedFolder()); // TODO: timeline preview // pCore->projectManager()->currentTimeline()->updatePreviewSettings(w->selectedPreview()); bool modified = false; if (m_renderWidget) { m_renderWidget->setDocumentPath(project->projectDataFolder() + QDir::separator()); } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { slotSwitchAudioThumbs(); } if (pCore->getCurrentProfile()->path() != profile || project->profileChanged(profile)) { pCore->setCurrentProfile(profile); pCore->projectManager()->slotResetProfiles(); slotUpdateDocumentState(true); } if (project->getDocumentProperty(QStringLiteral("proxyparams")) != w->proxyParams() || project->getDocumentProperty(QStringLiteral("proxyextension")) != w->proxyExtension()) { modified = true; project->setDocumentProperty(QStringLiteral("proxyparams"), w->proxyParams()); project->setDocumentProperty(QStringLiteral("proxyextension"), w->proxyExtension()); if (pCore->binController()->clipCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { // TODO: rebuild all proxies pCore->bin()->rebuildProxies(); } } if (project->getDocumentProperty(QStringLiteral("generateproxy")) != QString::number((int)w->generateProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyminsize")) != QString::number(w->proxyMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); } if (project->getDocumentProperty(QStringLiteral("generateimageproxy")) != QString::number((int)w->generateImageProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyimageminsize")) != QString::number(w->proxyImageMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); } if (QString::number((int)w->useProxy()) != project->getDocumentProperty(QStringLiteral("enableproxy"))) { project->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); modified = true; slotUpdateProxySettings(); } if (w->metadata() != project->metadata()) { project->setMetadata(w->metadata()); } QString newProjectFolder = w->storageFolder(); if (newProjectFolder.isEmpty()) { newProjectFolder = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } if (newProjectFolder != project->projectTempFolder()) { KMessageBox::ButtonCode answer; // Project folder changed: if (project->isModified()) { answer = KMessageBox::warningContinueCancel(this, i18n("The current project has not been saved. This will first save the project, then move " "all temporary files from %1 to %2, and the project file will be reloaded", project->projectTempFolder(), newProjectFolder)); if (answer == KMessageBox::Continue) { pCore->projectManager()->saveFile(); } } else { answer = KMessageBox::warningContinueCancel( this, i18n("This will move all temporary files from %1 to %2, the project file will then be reloaded", project->projectTempFolder(), newProjectFolder)); } if (answer == KMessageBox::Continue) { // Proceeed with move QString documentId = QDir::cleanPath(project->getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (!ok || documentId.isEmpty()) { KMessageBox::sorry(this, i18n("Cannot perform operation, invalid document id: %1", documentId)); } else { QDir newDir(newProjectFolder); QDir oldDir(project->projectTempFolder()); if (newDir.exists(documentId)) { KMessageBox::sorry(this, i18n("Cannot perform operation, target directory already exists: %1", newDir.absoluteFilePath(documentId))); } else { // Proceed with the move pCore->projectManager()->moveProjectData(oldDir.absoluteFilePath(documentId), newDir.absolutePath()); } } } } if (modified) { project->setModified(); } } delete w; } void MainWindow::slotDisableProxies() { pCore->currentDoc()->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)false)); pCore->currentDoc()->setModified(); slotUpdateProxySettings(); } void MainWindow::slotStopRenderProject() { if (m_renderWidget) { m_renderWidget->slotAbortCurrentJob(); } } void MainWindow::slotRenderProject() { KdenliveDoc *project = pCore->currentDoc(); if (!m_renderWidget) { QString projectfolder = project ? project->projectDataFolder() + QDir::separator() : KdenliveSettings::defaultprojectfolder(); if (project) { m_renderWidget = new RenderWidget(projectfolder, project->useProxy(), this); connect(m_renderWidget, &RenderWidget::shutdown, this, &MainWindow::slotShutdown); connect(m_renderWidget, &RenderWidget::selectedRenderProfile, this, &MainWindow::slotSetDocumentRenderProfile); connect(m_renderWidget, &RenderWidget::prepareRenderingData, this, &MainWindow::slotPrepareRendering); connect(m_renderWidget, &RenderWidget::abortProcess, this, &MainWindow::abortRenderJob); connect(m_renderWidget, &RenderWidget::openDvdWizard, this, &MainWindow::slotDvdWizard); connect(this, &MainWindow::updateRenderWidgetProfile, m_renderWidget, &RenderWidget::adjustViewToProfile); m_renderWidget->setGuides(project->getGuideModel()->getAllMarkers(), project->projectDuration()); m_renderWidget->setDocumentPath(project->projectDataFolder() + QDir::separator()); m_renderWidget->setRenderProfile(project->getRenderProperties()); } if (m_compositeAction->currentAction()) { - m_renderWidget->errorMessage(RenderWidget::CompositeError, - m_compositeAction->currentAction()->data().toInt() == 1 ? i18n("Rendering using low quality track compositing") - : QString()); + m_renderWidget->errorMessage(RenderWidget::CompositeError, m_compositeAction->currentAction()->data().toInt() == 1 + ? i18n("Rendering using low quality track compositing") + : QString()); } } slotCheckRenderStatus(); m_renderWidget->show(); // m_renderWidget->showNormal(); // What are the following lines supposed to do? // m_renderWidget->enableAudio(false); // m_renderWidget->export_audio; } void MainWindow::slotCheckRenderStatus() { // Make sure there are no missing clips // TODO /*if (m_renderWidget) m_renderWidget->missingClips(pCore->bin()->hasMissingClips());*/ } void MainWindow::setRenderingProgress(const QString &url, int progress) { emit setRenderProgress(progress); if (m_renderWidget) { m_renderWidget->setRenderJob(url, progress); } } void MainWindow::setRenderingFinished(const QString &url, int status, const QString &error) { emit setRenderProgress(100); if (m_renderWidget) { m_renderWidget->setRenderStatus(url, status, error); } } void MainWindow::addProjectClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->binController()->getBinIdsByResource(QFileInfo(url)); if (!ids.isEmpty()) { // Clip is already in project bin, abort return; } - ClipCreationDialog::createClipsCommand(pCore->currentDoc(), QList() << QUrl::fromLocalFile(url), QStringList(), pCore->bin()); + + ClipCreator::createClipFromFile(url, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel()); } } void MainWindow::addTimelineClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->binController()->getBinIdsByResource(QFileInfo(url)); if (!ids.isEmpty()) { pCore->bin()->selectClipById(ids.constFirst()); slotInsertClipInsert(); } } } void MainWindow::addEffect(const QString &effectName) { QStringList effectInfo; effectInfo << effectName << effectName; const QDomElement effect = EffectsListWidget::itemEffect(5, effectInfo); if (!effect.isNull()) { slotAddEffect(effect); } else { qCDebug(KDENLIVE_LOG) << " * * *EFFECT: " << effectName << " NOT AVAILABLE"; exitApp(); } } void MainWindow::scriptRender(const QString &url) { slotRenderProject(); m_renderWidget->slotPrepareExport(true, url); } void MainWindow::exitApp() { QApplication::exit(0); } void MainWindow::slotCleanProject() { if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) { return; } pCore->bin()->cleanup(); } void MainWindow::slotUpdateMousePosition(int pos) { if (pCore->currentDoc()) { switch (m_timeFormatButton->currentItem()) { case 0: - m_timeFormatButton->setText( - pCore->currentDoc()->timecode().getTimecodeFromFrames(pos) + QStringLiteral(" / ") + - pCore->currentDoc()->timecode().getTimecodeFromFrames(getMainTimeline()->controller()->duration())); + m_timeFormatButton->setText(pCore->currentDoc()->timecode().getTimecodeFromFrames(pos) + QStringLiteral(" / ") + + pCore->currentDoc()->timecode().getTimecodeFromFrames(getMainTimeline()->controller()->duration())); break; default: - m_timeFormatButton->setText(QStringLiteral("%1 / %2") - .arg(pos, 6, 10, QLatin1Char('0')) - .arg(getMainTimeline()->controller()->duration(), 6, 10, QLatin1Char('0'))); + m_timeFormatButton->setText( + QStringLiteral("%1 / %2").arg(pos, 6, 10, QLatin1Char('0')).arg(getMainTimeline()->controller()->duration(), 6, 10, QLatin1Char('0'))); } } } void MainWindow::slotUpdateProjectDuration(int pos) { if (pCore->currentDoc()) { slotUpdateMousePosition(getMainTimeline()->controller()->getMousePos()); } } void MainWindow::slotUpdateDocumentState(bool modified) { setWindowTitle(pCore->currentDoc()->description()); setWindowModified(modified); m_saveAction->setEnabled(modified); } void MainWindow::connectDocument() { KdenliveDoc *project = pCore->currentDoc(); connect(project, &KdenliveDoc::startAutoSave, pCore->projectManager(), &ProjectManager::slotStartAutoSave); connect(project, &KdenliveDoc::reloadEffects, this, &MainWindow::slotReloadEffects); KdenliveSettings::setProject_fps(pCore->getCurrentFps()); // TODO REFAC: reconnect to new timeline /* Timeline *trackView = pCore->projectManager()->currentTimeline(); connect(trackView, &Timeline::configTrack, this, &MainWindow::slotConfigTrack); connect(trackView, &Timeline::updateTracksInfo, this, &MainWindow::slotUpdateTrackInfo); connect(trackView, &Timeline::mousePosition, this, &MainWindow::slotUpdateMousePosition); connect(pCore->producerQueue(), &ProducerQueue::infoProcessingFinished, trackView->projectView(), &CustomTrackView::slotInfoProcessingFinished, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::importKeyframes, this, &MainWindow::slotProcessImportKeyframes); connect(trackView->projectView(), &CustomTrackView::updateTrimMode, this, &MainWindow::setTrimMode); connect(m_projectMonitor, &Monitor::multitrackView, trackView, &Timeline::slotMultitrackView); connect(m_projectMonitor, SIGNAL(renderPosition(int)), trackView, SLOT(moveCursorPos(int))); connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), trackView, SLOT(slotSetZone(QPoint))); connect(trackView->projectView(), &CustomTrackView::guidesUpdated, this, &MainWindow::slotGuidesUpdated); connect(trackView->projectView(), &CustomTrackView::loadMonitorScene, m_projectMonitor, &Monitor::slotShowEffectScene); connect(trackView->projectView(), &CustomTrackView::setQmlProperty, m_projectMonitor, &Monitor::setQmlProperty); connect(m_projectMonitor, SIGNAL(acceptRipple(bool)), trackView->projectView(), SLOT(slotAcceptRipple(bool))); connect(m_projectMonitor, SIGNAL(switchTrimMode(int)), trackView->projectView(), SLOT(switchTrimMode(int))); connect(project, &KdenliveDoc::saveTimelinePreview, trackView, &Timeline::slotSaveTimelinePreview); connect(trackView, SIGNAL(showTrackEffects(int, TrackInfo)), this, SLOT(slotTrackSelected(int, TrackInfo))); connect(trackView->projectView(), &CustomTrackView::clipItemSelected, this, &MainWindow::slotTimelineClipSelected, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::setActiveKeyframe, m_effectStack, &EffectStackView2::setActiveKeyframe); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_effectStack, SLOT(slotTransitionItemSelected(Transition *, int, QPoint, bool)), Qt::DirectConnection); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), this, SLOT(slotActivateTransitionView(Transition *))); connect(trackView->projectView(), &CustomTrackView::zoomIn, this, &MainWindow::slotZoomIn); connect(trackView->projectView(), &CustomTrackView::zoomOut, this, &MainWindow::slotZoomOut); connect(trackView, SIGNAL(setZoom(int)), this, SLOT(slotSetZoom(int))); connect(trackView, SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(trackView->projectView(), SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(pCore->bin(), &Bin::clipNameChanged, trackView->projectView(), &CustomTrackView::clipNameChanged); connect(trackView->projectView(), SIGNAL(showClipFrame(QString, int)), pCore->bin(), SLOT(selectClipById(QString, int))); connect(trackView->projectView(), SIGNAL(playMonitor()), m_projectMonitor, SLOT(slotPlay())); connect(trackView->projectView(), &CustomTrackView::pauseMonitor, m_projectMonitor, &Monitor::pause, Qt::DirectConnection); connect(m_projectMonitor, &Monitor::addEffect, trackView->projectView(), &CustomTrackView::slotAddEffectToCurrentItem); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_projectMonitor, SLOT(slotSetSelectedClip(Transition *))); connect(pCore->bin(), SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap)), trackView->projectView(), SLOT(slotGotFilterJobResults(QString, int, int, stringMap, stringMap))); //TODO //connect(m_projectList, SIGNAL(addMarkers(QString,QList)), trackView->projectView(), SLOT(slotAddClipMarker(QString,QList))); // Effect stack signals connect(m_effectStack, &EffectStackView2::updateEffect, trackView->projectView(), &CustomTrackView::slotUpdateClipEffect); connect(m_effectStack, &EffectStackView2::updateClipRegion, trackView->projectView(), &CustomTrackView::slotUpdateClipRegion); connect(m_effectStack, SIGNAL(removeEffect(ClipItem *, int, QDomElement)), trackView->projectView(), SLOT(slotDeleteEffect(ClipItem *, int, QDomElement))); connect(m_effectStack, SIGNAL(removeEffectGroup(ClipItem *, int, QDomDocument)), trackView->projectView(), SLOT(slotDeleteEffectGroup(ClipItem *, int, QDomDocument))); connect(m_effectStack, SIGNAL(addEffect(ClipItem *, QDomElement, int)), trackView->projectView(), SLOT(slotAddEffect(ClipItem *, QDomElement, int))); connect(m_effectStack, SIGNAL(changeEffectState(ClipItem *, int, QList, bool)), trackView->projectView(), SLOT(slotChangeEffectState(ClipItem *, int, QList, bool))); connect(m_effectStack, SIGNAL(changeEffectPosition(ClipItem *, int, QList, int)), trackView->projectView(), SLOT(slotChangeEffectPosition(ClipItem *, int, QList, int))); connect(m_effectStack, &EffectStackView2::refreshEffectStack, trackView->projectView(), &CustomTrackView::slotRefreshEffects); connect(m_effectStack, &EffectStackView2::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(m_effectStack, SIGNAL(importClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap)), trackView->projectView(), SLOT(slotImportClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap))); // Transition config signals connect(m_effectStack->transitionConfig(), SIGNAL(transitionUpdated(Transition *, QDomElement)), trackView->projectView(), SLOT(slotTransitionUpdated(Transition *, QDomElement))); connect(m_effectStack->transitionConfig(), &TransitionSettings::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(trackView->projectView(), SIGNAL(activateDocumentMonitor()), m_projectMonitor, SLOT(slotActivateMonitor()), Qt::DirectConnection); connect(project, &KdenliveDoc::updateFps, this, &MainWindow::slotUpdateProfile, Qt::DirectConnection); connect(trackView, &Timeline::zoneMoved, this, &MainWindow::slotZoneMoved); trackView->projectView()->setContextMenu(m_timelineContextMenu, m_timelineContextClipMenu, m_timelineContextTransitionMenu, m_clipTypeGroup, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); */ connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(project, &KdenliveDoc::docModified, this, &MainWindow::slotUpdateDocumentState); connect(pCore->bin(), SIGNAL(displayMessage(QString, int, MessageType)), m_messageLabel, SLOT(setProgressMessage(QString, int, MessageType))); if (m_renderWidget) { slotCheckRenderStatus(); // m_renderWidget->setGuides(pCore->projectManager()->currentTimeline()->projectView()->guidesData(), project->projectDuration()); m_renderWidget->setDocumentPath(project->projectDataFolder() + QDir::separator()); m_renderWidget->setRenderProfile(project->getRenderProperties()); } m_zoomSlider->setValue(project->zoom().x()); m_commandStack->setActiveStack(project->commandStack().get()); setWindowTitle(project->description()); setWindowModified(project->isModified()); m_saveAction->setEnabled(project->isModified()); m_normalEditTool->setChecked(true); connect(m_projectMonitor, &Monitor::durationChanged, this, &MainWindow::slotUpdateProjectDuration); pCore->monitorManager()->setDocument(project); // TODO REFAC: fix // trackView->updateProfile(1.0); // Init document zone // m_projectMonitor->slotZoneMoved(trackView->inPoint(), trackView->outPoint()); // Update the mouse position display so it will display in DF/NDF format by default based on the project setting. // slotUpdateMousePosition(0); // Update guides info in render widget // slotGuidesUpdated(); // set tool to select tool setTrimMode(QString()); m_buttonSelectTool->setChecked(true); connect(m_projectMonitorDock, &QDockWidget::visibilityChanged, m_projectMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); connect(m_clipMonitorDock, &QDockWidget::visibilityChanged, m_clipMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); } void MainWindow::slotZoneMoved(int start, int end) { pCore->currentDoc()->setZone(start, end); QPoint zone(start, end); m_projectMonitor->slotLoadClipZone(zone); } void MainWindow::slotGuidesUpdated() { if (m_renderWidget) { m_renderWidget->setGuides(pCore->currentDoc()->getGuideModel()->getAllMarkers(), pCore->currentDoc()->projectDuration()); } } void MainWindow::slotEditKeys() { KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); dialog.addCollection(actionCollection(), i18nc("general keyboard shortcuts", "General")); dialog.configure(); } void MainWindow::slotPreferences(int page, int option) { /* * An instance of your dialog could be already created and could be * cached, in which case you want to display the cached dialog * instead of creating another one */ if (KConfigDialog::showDialog(QStringLiteral("settings"))) { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (page != -1) { d->showPage(page, option); } return; } // KConfigDialog didn't find an instance of this dialog, so lets // create it : // Get the mappable actions in localized form QMap actions; KActionCollection *collection = actionCollection(); QRegExp ampEx("&{1,1}"); for (const QString &action_name : m_actionNames) { QString action_text = collection->action(action_name)->text(); action_text.remove(ampEx); actions[action_text] = action_name; } auto *dialog = new KdenliveSettingsDialog(actions, m_gpuAllowed, this); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::updateConfiguration); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::configurationChanged); connect(dialog, &KdenliveSettingsDialog::doResetProfile, pCore->projectManager(), &ProjectManager::slotResetProfiles); connect(dialog, &KdenliveSettingsDialog::checkTabPosition, this, &MainWindow::slotCheckTabPosition); connect(dialog, &KdenliveSettingsDialog::restartKdenlive, this, &MainWindow::slotRestart); connect(dialog, &KdenliveSettingsDialog::updateLibraryFolder, pCore.get(), &Core::updateLibraryPath); connect(dialog, &KdenliveSettingsDialog::audioThumbFormatChanged, m_timelineTabs, &TimelineTabs::audioThumbFormatChanged); dialog->show(); if (page != -1) { dialog->showPage(page, option); } } void MainWindow::slotCheckTabPosition() { int pos = tabPosition(Qt::LeftDockWidgetArea); if (KdenliveSettings::tabposition() != pos) { setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); } } void MainWindow::slotRestart() { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } void MainWindow::closeEvent(QCloseEvent *event) { KXmlGuiWindow::closeEvent(event); if (event->isAccepted()) { #ifdef Q_OS_WIN QProcess::startDetached(QStandardPaths::findExecutable(QStringLiteral("kdeinit5")) + " --terminate"); #endif QApplication::exit(m_exitCode); return; } } void MainWindow::updateConfiguration() { // TODO: we should apply settings to all projects, not only the current one m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); slotSwitchSplitAudio(KdenliveSettings::splitaudio()); slotSwitchAutomaticTransition(); // Update list of transcoding profiles buildDynamicActions(); loadClipActions(); } void MainWindow::slotSwitchSplitAudio(bool enable) { KdenliveSettings::setSplitaudio(enable); m_buttonAutomaticSplitAudio->setChecked(KdenliveSettings::splitaudio()); - //TODO update leds on split mode? + // TODO update leds on split mode? /*if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->updateHeaders(); }*/ } void MainWindow::slotSwitchVideoThumbs() { KdenliveSettings::setVideothumbnails(!KdenliveSettings::videothumbnails()); m_timelineTabs->showThumbnailsChanged(); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); } void MainWindow::slotSwitchAudioThumbs() { KdenliveSettings::setAudiothumbnails(!KdenliveSettings::audiothumbnails()); pCore->binController()->checkAudioThumbs(); m_timelineTabs->showAudioThumbnailsChanged(); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); } void MainWindow::slotSwitchMarkersComments() { KdenliveSettings::setShowmarkers(!KdenliveSettings::showmarkers()); getMainTimeline()->controller()->showMarkersChanged(); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); } void MainWindow::slotSwitchSnap() { KdenliveSettings::setSnaptopoints(!KdenliveSettings::snaptopoints()); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); getMainTimeline()->controller()->snapChanged(KdenliveSettings::snaptopoints()); } void MainWindow::slotSwitchAutomaticTransition() { KdenliveSettings::setAutomatictransitions(!KdenliveSettings::automatictransitions()); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); } void MainWindow::slotDeleteItem() { if ((QApplication::focusWidget() != nullptr) && (QApplication::focusWidget()->parentWidget() != nullptr) && QApplication::focusWidget()->parentWidget() == pCore->bin()) { pCore->bin()->slotDeleteClip(); } else { QWidget *widget = QApplication::focusWidget(); while ((widget != nullptr) && widget != this) { if (widget == m_effectStackDock) { // TODO refac: reimplement // m_effectStack->deleteCurrentEffect(); return; } widget = widget->parentWidget(); } // effect stack has no focus getMainTimeline()->controller()->deleteSelectedClips(); } } void MainWindow::slotAddClipMarker() { KdenliveDoc *project = pCore->currentDoc(); std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { return; } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); clip->getMarkerModel()->editMarkerGui(pos, this, true, clip.get()); } void MainWindow::slotDeleteClipMarker(bool allowGuideDeletion) { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { - //TODO refac retrieve active clip + // TODO refac retrieve active clip /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime marker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { if (allowGuideDeletion && m_projectMonitor->isActive()) { slotDeleteGuide(); } else { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); } return; } clip->getMarkerModel()->removeMarker(pos); } void MainWindow::slotDeleteAllClipMarkers() { std::shared_ptr clip(nullptr); if (m_projectMonitor->isActive()) { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } bool ok = clip->getMarkerModel()->removeAllMarkers(); if (!ok) { m_messageLabel->setMessage(i18n("An error occured while deleting markers"), ErrorMessage); return; } } void MainWindow::slotEditClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to edit marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime oldMarker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); return; } clip->getMarkerModel()->editMarkerGui(pos, this, false, clip.get()); } void MainWindow::slotAddMarkerGuideQuickly() { if (!getMainTimeline() || !pCore->currentDoc()) { return; } if (m_clipMonitor->isActive()) { std::shared_ptr clip(m_clipMonitor->currentController()); GenTime pos(m_clipMonitor->position(), pCore->getCurrentFps()); if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } else { getMainTimeline()->controller()->switchGuide(); } } void MainWindow::slotAddGuide() { getMainTimeline()->controller()->switchGuide(); } void MainWindow::slotInsertSpace() { getMainTimeline()->controller()->insertSpace(); } void MainWindow::slotRemoveSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, false); } void MainWindow::slotRemoveAllSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, true); } void MainWindow::slotInsertTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); // TODO refac /* if (pCore->projectManager()->currentTimeline()) { int ix = pCore->projectManager()->currentTimeline()->projectView()->selectedTrack(); pCore->projectManager()->currentTimeline()->projectView()->slotInsertTrack(ix); } */ } void MainWindow::slotDeleteTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); // TODO refac /* if (pCore->projectManager()->currentTimeline()) { int ix = pCore->projectManager()->currentTimeline()->projectView()->selectedTrack(); pCore->projectManager()->currentTimeline()->projectView()->slotDeleteTrack(ix); } */ } void MainWindow::slotConfigTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); // TODO refac /* if (pCore->projectManager()->currentTimeline()) { int ix = pCore->projectManager()->currentTimeline()->projectView()->selectedTrack(); pCore->projectManager()->currentTimeline()->projectView()->slotConfigTracks(ix); } */ } void MainWindow::slotSelectTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->slotSelectClipsInTrack(); } */ } void MainWindow::slotSelectAllTracks() { // TODO refac /* pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->slotSelectAllClips(); } */ } void MainWindow::slotUnselectAllTracks() { // TODO refac /* pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->clearSelection(); } */ } void MainWindow::slotEditGuide() { getMainTimeline()->controller()->editGuide(); } void MainWindow::slotDeleteGuide() { getMainTimeline()->controller()->switchGuide(-1, true); } void MainWindow::slotDeleteAllGuides() { pCore->currentDoc()->getGuideModel()->removeAllMarkers(); } void MainWindow::slotCutTimelineClip() { getMainTimeline()->controller()->cutClipUnderCursor(); } void MainWindow::slotInsertClipOverwrite() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), true); } void MainWindow::slotInsertClipInsert() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), false); } void MainWindow::slotExtractZone() { getMainTimeline()->controller()->extractZone(); } void MainWindow::slotLiftZone() { getMainTimeline()->controller()->liftZone(); } void MainWindow::slotPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->startPreviewRender(); } } void MainWindow::slotStopPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->stopPreviewRender(); } } void MainWindow::slotDefinePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(true); } } void MainWindow::slotRemovePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(false); } } void MainWindow::slotClearPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->clearPreviewRange(); } } void MainWindow::slotSelectTimelineClip() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectClip(true); } */ } void MainWindow::slotSelectTimelineTransition() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectTransition(true); } */ } void MainWindow::slotDeselectTimelineClip() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectClip(false, true); } */ } void MainWindow::slotDeselectTimelineTransition() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectTransition(false, true); } */ } void MainWindow::slotSelectAddTimelineClip() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectClip(true, true); } */ } void MainWindow::slotSelectAddTimelineTransition() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->selectTransition(true, true); } */ } void MainWindow::slotGroupClips() { getCurrentTimeline()->controller()->groupSelection(); } void MainWindow::slotUnGroupClips() { getCurrentTimeline()->controller()->unGroupSelection(); } void MainWindow::slotEditItemDuration() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->editItemDuration(); } */ } void MainWindow::slotAddProjectClip(const QUrl &url, const QStringList &folderInfo) { pCore->bin()->droppedUrls(QList() << url, folderInfo); } void MainWindow::slotAddProjectClipList(const QList &urls) { pCore->bin()->droppedUrls(urls); } void MainWindow::slotAddTransition(QAction *result) { if (!result) { return; } - //TODO refac + // TODO refac /* QStringList info = result->data().toStringList(); if (info.isEmpty() || info.count() < 2) { return; } QDomElement transition = transitions.getEffectByTag(info.at(0), info.at(1)); if (pCore->projectManager()->currentTimeline() && !transition.isNull()) { pCore->projectManager()->currentTimeline()->projectView()->slotAddTransitionToSelectedClips(transition.cloneNode().toElement()); } */ } void MainWindow::slotAddVideoEffect(QAction *result) { if (!result) { return; } QStringList info = result->data().toStringList(); if (info.isEmpty() || info.size() < 3) { return; } QDomElement effect; int effectType = info.last().toInt(); switch (effectType) { case EffectsList::EFFECT_VIDEO: case EffectsList::EFFECT_GPU: effect = videoEffects.getEffectByTag(info.at(0), info.at(1)); break; case EffectsList::EFFECT_AUDIO: effect = audioEffects.getEffectByTag(info.at(0), info.at(1)); break; case EffectsList::EFFECT_CUSTOM: effect = customEffects.getEffectByTag(info.at(0), info.at(1)); break; default: effect = videoEffects.getEffectByTag(info.at(0), info.at(1)); if (!effect.isNull()) { break; } effect = audioEffects.getEffectByTag(info.at(0), info.at(1)); if (!effect.isNull()) { break; } effect = customEffects.getEffectByTag(info.at(0), info.at(1)); break; } if (!effect.isNull()) { slotAddEffect(effect); } else { m_messageLabel->setMessage(i18n("Cannot find effect %1 / %2", info.at(0), info.at(1)), ErrorMessage); } } void MainWindow::slotZoomIn(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() - 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotZoomOut(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() + 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotFitZoom() { /* if (pCore->projectManager()->currentTimeline()) { m_zoomSlider->setValue(pCore->projectManager()->currentTimeline()->fitZoom()); // Make sure to reset scroll bar to start pCore->projectManager()->currentTimeline()->projectView()->scrollToStart(); } */ } void MainWindow::slotSetZoom(int value, bool zoomOnMouse) { value = qBound(m_zoomSlider->minimum(), value, m_zoomSlider->maximum()); m_timelineTabs->changeZoom(value, zoomOnMouse); m_zoomOut->setEnabled(value < m_zoomSlider->maximum()); m_zoomIn->setEnabled(value > m_zoomSlider->minimum()); slotUpdateZoomSliderToolTip(value); m_zoomSlider->blockSignals(true); m_zoomSlider->setValue(value); m_zoomSlider->blockSignals(false); } void MainWindow::slotShowZoomSliderToolTip(int zoomlevel) { if (zoomlevel != -1) { slotUpdateZoomSliderToolTip(zoomlevel); } QPoint global = m_zoomSlider->rect().topLeft(); global.ry() += m_zoomSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_zoomSlider->mapToGlobal(global)); QApplication::sendEvent(m_zoomSlider, &toolTipEvent); } void MainWindow::slotUpdateZoomSliderToolTip(int zoomlevel) { m_zoomSlider->setToolTip(i18n("Zoom Level: %1/13", (13 - zoomlevel))); } void MainWindow::slotGotProgressInfo(const QString &message, int progress, MessageType type) { m_messageLabel->setProgressMessage(message, progress, type); } void MainWindow::customEvent(QEvent *e) { if (e->type() == QEvent::User) { m_messageLabel->setMessage(static_cast(e)->message(), MltError); } } void MainWindow::slotSnapRewind() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoPreviousSnap(); } else { m_clipMonitor->slotSeekToPreviousSnap(); } } void MainWindow::slotSnapForward() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoNextSnap(); } else { m_clipMonitor->slotSeekToNextSnap(); } } void MainWindow::slotClipStart() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(false); } } void MainWindow::slotClipEnd() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(true); } } void MainWindow::slotChangeTool(QAction *action) { if (action == m_buttonSelectTool) { slotSetTool(SelectTool); } else if (action == m_buttonRazorTool) { slotSetTool(RazorTool); } else if (action == m_buttonSpacerTool) { slotSetTool(SpacerTool); } } void MainWindow::slotChangeEdit(QAction *action) { - //TODO refac + // TODO refac /* if (!pCore->projectManager()->currentTimeline()) { return; } if (action == m_overwriteEditTool) { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::OverwriteEdit); } else if (action == m_insertEditTool) { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::InsertEdit); } else { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::NormalEdit); } */ } void MainWindow::slotSetTool(ProjectTool tool) { if (pCore->currentDoc()) { // pCore->currentDoc()->setTool(tool); QString message; switch (tool) { case SpacerTool: message = i18n("Ctrl + click to use spacer on current track only"); break; case RazorTool: message = i18n("Click on a clip to cut it, Shift + move to preview cut frame"); break; default: message = i18n("Shift + click to create a selection rectangle, Ctrl + click to add an item to selection"); break; } m_messageLabel->setMessage(message, InformationMessage); getMainTimeline()->setTool(tool); } } void MainWindow::slotCopy() { getMainTimeline()->controller()->copyItem(); } void MainWindow::slotPaste() { getMainTimeline()->controller()->pasteItem(); } void MainWindow::slotPasteEffects() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->pasteClipEffects(); } */ } -void MainWindow::slotClipInTimeline(const QString &clipId, QList ids) +void MainWindow::slotClipInTimeline(const QString &clipId, QList ids) { QMenu *inTimelineMenu = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); QList actionList; for (int i = 0; i < ids.count(); ++i) { QString track = getMainTimeline()->controller()->getTrackNameFromIndex(pCore->getItemTrack(ObjectId(ObjectType::TimelineClip, ids.at(i)))); QString start = pCore->currentDoc()->timecode().getTimecodeFromFrames(pCore->getItemIn(ObjectId(ObjectType::TimelineClip, ids.at(i)))); int j = 0; QAction *a = new QAction(track + QStringLiteral(": ") + start, inTimelineMenu); a->setData(ids.at(i)); connect(a, &QAction::triggered, this, &MainWindow::slotSelectClipInTimeline); while (j < actionList.count()) { if (actionList.at(j)->text() > a->text()) { break; } j++; } actionList.insert(j, a); } QList list = inTimelineMenu->actions(); unplugActionList(QStringLiteral("timeline_occurences")); qDeleteAll(list); plugActionList(QStringLiteral("timeline_occurences"), actionList); if (actionList.isEmpty()) { inTimelineMenu->setEnabled(false); } else { inTimelineMenu->setEnabled(true); } } void MainWindow::slotClipInProjectTree() { - QList ids = getMainTimeline()->controller()->selection(); + QList ids = getMainTimeline()->controller()->selection(); if (!ids.isEmpty()) { m_projectBinDock->raise(); ObjectId id(ObjectType::TimelineClip, ids.constFirst()); int start = pCore->getItemIn(id); QPoint zone(start, start + pCore->getItemDuration(id)); if (m_projectMonitor->isActive()) { slotSwitchMonitors(); } pCore->bin()->selectClipById(getMainTimeline()->controller()->getClipBinId(ids.constFirst()), -1, zone); } } void MainWindow::slotSelectClipInTimeline() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); QAction *action = qobject_cast(sender()); int clipId = action->data().toInt(); getMainTimeline()->controller()->focusItem(clipId); } /** Gets called when the window gets hidden */ void MainWindow::hideEvent(QHideEvent * /*event*/) { if (isMinimized() && pCore->monitorManager()) { pCore->monitorManager()->pauseActiveMonitor(); } } /*void MainWindow::slotSaveZone(Render *render, const QPoint &zone, DocClipBase *baseClip, QUrl path) { QPointer dialog = new QDialog(this); dialog->setWindowTitle("Save clip zone"); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); QLabel *label1 = new QLabel(i18n("Save clip zone as:"), this); if (path.isEmpty()) { QString tmppath = pCore->currentDoc()->projectFolder().path() + QDir::separator(); if (baseClip == nullptr) { tmppath.append("untitled.mlt"); } else { tmppath.append((baseClip->name().isEmpty() ? baseClip->fileURL().fileName() : baseClip->name()) + '-' + QString::number(zone.x()).rightJustified(4, '0') + QStringLiteral(".mlt")); } path = QUrl(tmppath); } KUrlRequester *url = new KUrlRequester(path, this); url->setFilter("video/mlt-playlist"); QLabel *label2 = new QLabel(i18n("Description:"), this); QLineEdit *edit = new QLineEdit(this); mainLayout->addWidget(label1); mainLayout->addWidget(url); mainLayout->addWidget(label2); mainLayout->addWidget(edit); mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { if (QFile::exists(url->url().path())) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", url->url().path())) == KMessageBox::No) { slotSaveZone(render, zone, baseClip, url->url()); delete dialog; return; } } if (baseClip && !baseClip->fileURL().isEmpty()) { // create zone from clip url, so that we don't have problems with proxy clips QProcess p; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("MLT_PROFILE"); p.setProcessEnvironment(env); p.start(KdenliveSettings::rendererpath(), QStringList() << baseClip->fileURL().toLocalFile() << "in=" + QString::number(zone.x()) << "out=" + QString::number(zone.y()) << "-consumer" << "xml:" + url->url().path()); if (!p.waitForStarted(3000)) { KMessageBox::sorry(this, i18n("Cannot start MLT's renderer:\n%1", KdenliveSettings::rendererpath())); } else if (!p.waitForFinished(5000)) { KMessageBox::sorry(this, i18n("Timeout while creating xml output")); } } else render->saveZone(url->url(), edit->text(), zone); } delete dialog; }*/ void MainWindow::slotResizeItemStart() { getMainTimeline()->controller()->setInPoint(); } void MainWindow::slotResizeItemEnd() { getMainTimeline()->controller()->setOutPoint(); } int MainWindow::getNewStuff(const QString &configFile) { KNS3::Entry::List entries; QPointer dialog = new KNS3::DownloadDialog(configFile); if (dialog->exec() != 0) { entries = dialog->changedEntries(); } for (const KNS3::Entry &entry : entries) { if (entry.status() == KNS3::Entry::Installed) { qCDebug(KDENLIVE_LOG) << "// Installed files: " << entry.installedFiles(); } } delete dialog; return entries.size(); } void MainWindow::slotGetNewTitleStuff() { if (getNewStuff(QStringLiteral("kdenlive_titles.knsrc")) > 0) { // get project title path QString titlePath = pCore->currentDoc()->projectDataFolder() + QStringLiteral("/titles/"); TitleWidget::refreshTitleTemplates(titlePath); } } void MainWindow::slotGetNewLumaStuff() { if (getNewStuff(QStringLiteral("kdenlive_wipes.knsrc")) > 0) { initEffects::refreshLumas(); // TODO: refresh currently displayd trans ? } } void MainWindow::slotGetNewRenderStuff() { if (getNewStuff(QStringLiteral("kdenlive_renderprofiles.knsrc")) > 0) if (m_renderWidget) { m_renderWidget->reloadProfiles(); } } void MainWindow::slotAutoTransition() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->autoTransition(); } */ } void MainWindow::slotSplitAudio() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->splitAudio(); } */ } void MainWindow::slotSetAudioAlignReference() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->setAudioAlignReference(); } */ } void MainWindow::slotAlignAudio() { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->alignAudio(); } */ } void MainWindow::slotUpdateClipType(QAction *action) { - //TODO refac + // TODO refac /* if (pCore->projectManager()->currentTimeline()) { PlaylistState::ClipState state = (PlaylistState::ClipState)action->data().toInt(); pCore->projectManager()->currentTimeline()->projectView()->setClipType(state); } */ } void MainWindow::slotDvdWizard(const QString &url) { // We must stop the monitors since we create a new on in the dvd wizard QPointer w = new DvdWizard(pCore->monitorManager(), url, this); w->exec(); delete w; pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } void MainWindow::slotShowTimeline(bool show) { if (!show) { m_timelineState = saveState(); centralWidget()->setHidden(true); } else { centralWidget()->setHidden(false); restoreState(m_timelineState); } } void MainWindow::loadClipActions() { unplugActionList(QStringLiteral("add_effect")); plugActionList(QStringLiteral("add_effect"), m_effectsMenu->actions()); QList clipJobActions = getExtraActions(QStringLiteral("clipjobs")); unplugActionList(QStringLiteral("clip_jobs")); plugActionList(QStringLiteral("clip_jobs"), clipJobActions); QList atcActions = getExtraActions(QStringLiteral("audiotranscoderslist")); unplugActionList(QStringLiteral("audio_transcoders_list")); plugActionList(QStringLiteral("audio_transcoders_list"), atcActions); QList tcActions = getExtraActions(QStringLiteral("transcoderslist")); unplugActionList(QStringLiteral("transcoders_list")); plugActionList(QStringLiteral("transcoders_list"), tcActions); } void MainWindow::loadDockActions() { QList list = kdenliveCategoryMap.value(QStringLiteral("interface"))->actions(); // Sort actions QMap sorted; QStringList sortedList; for (QAction *a : list) { sorted.insert(a->text(), a); sortedList << a->text(); } QList orderedList; sortedList.sort(Qt::CaseInsensitive); for (const QString &text : sortedList) { orderedList << sorted.value(text); } unplugActionList(QStringLiteral("dock_actions")); plugActionList(QStringLiteral("dock_actions"), orderedList); } void MainWindow::buildDynamicActions() { KActionCategory *ts = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("clipjobs"))) { ts = kdenliveCategoryMap.take(QStringLiteral("clipjobs")); delete ts; } ts = new KActionCategory(i18n("Clip Jobs"), m_extraFactory->actionCollection()); Mlt::Profile profile; - Mlt::Filter *filter; + std::unique_ptr filter; for (const QString &stab : {QStringLiteral("vidstab"), QStringLiteral("videostab2"), QStringLiteral("videostab")}) { - filter = Mlt::Factory::filter(profile, (char *)stab.toUtf8().constData()); + filter.reset(Mlt::Factory::filter(profile, (char *)stab.toUtf8().constData())); if ((filter != nullptr) && filter->is_valid()) { QAction *action = new QAction(i18n("Stabilize") + QStringLiteral(" (") + stab + QLatin1Char(')'), m_extraFactory->actionCollection()); - action->setData(QStringList() << QString::number((int)AbstractClipJob::FILTERCLIPJOB) << stab); ts->addAction(action->text(), action); - connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); - delete filter; + connect(action, &QAction::triggered, + [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Stabilize clips"), stab); }); break; } - delete filter; } - filter = Mlt::Factory::filter(profile, (char*)"motion_est"); + filter.reset(Mlt::Factory::filter(profile, (char *)"motion_est")); if (filter) { if (filter->is_valid()) { QAction *action = new QAction(i18n("Automatic scene split"), m_extraFactory->actionCollection()); - QStringList stabJob; - stabJob << QString::number((int)AbstractClipJob::FILTERCLIPJOB) << QStringLiteral("motion_est"); - action->setData(stabJob); ts->addAction(action->text(), action); - connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); + connect(action, &QAction::triggered, + [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Stabilize clips")); }); } - delete filter; } + // TODO refac see if we want to reimplement speed change job. If so, maybe use better algorithm? + /* if (KdenliveSettings::producerslist().contains(QStringLiteral("timewarp"))) { QAction *action = new QAction(i18n("Duplicate clip with speed change"), m_extraFactory->actionCollection()); QStringList stabJob; stabJob << QString::number((int)AbstractClipJob::FILTERCLIPJOB) << QStringLiteral("timewarp"); action->setData(stabJob); ts->addAction(action->text(), action); connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); } + */ + // TODO refac reimplement analyseclipjob + /* QAction *action = new QAction(i18n("Analyse keyframes"), m_extraFactory->actionCollection()); QStringList stabJob(QString::number((int)AbstractClipJob::ANALYSECLIPJOB)); action->setData(stabJob); ts->addAction(action->text(), action); connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); + */ kdenliveCategoryMap.insert(QStringLiteral("clipjobs"), ts); if (kdenliveCategoryMap.contains(QStringLiteral("transcoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("transcoderslist")); delete ts; } if (kdenliveCategoryMap.contains(QStringLiteral("audiotranscoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("audiotranscoderslist")); delete ts; } + // TODO refac : reimplement transcode + /* ts = new KActionCategory(i18n("Transcoders"), m_extraFactory->actionCollection()); KActionCategory *ats = new KActionCategory(i18n("Extract Audio"), m_extraFactory->actionCollection()); KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenlivetranscodingrc")), KConfig::CascadeConfig); KConfigGroup transConfig(config, "Transcoding"); // read the entries QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); QStringList transList; transList << QString::number((int)AbstractClipJob::TRANSCODEJOB); transList << i.value().split(QLatin1Char(';')); auto *a = new QAction(i.key(), m_extraFactory->actionCollection()); a->setData(transList); if (transList.count() > 1) { a->setToolTip(transList.at(1)); } // slottranscode connect(a, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); if (transList.count() > 3 && transList.at(3) == QLatin1String("audio")) { // This is an audio transcoding action ats->addAction(i.key(), a); } else { ts->addAction(i.key(), a); } } kdenliveCategoryMap.insert(QStringLiteral("transcoderslist"), ts); kdenliveCategoryMap.insert(QStringLiteral("audiotranscoderslist"), ats); + */ // Populate View menu with show / hide actions for dock widgets KActionCategory *guiActions = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("interface"))) { guiActions = kdenliveCategoryMap.take(QStringLiteral("interface")); delete guiActions; } guiActions = new KActionCategory(i18n("Interface"), actionCollection()); QAction *showTimeline = new QAction(i18n("Timeline"), this); showTimeline->setCheckable(true); showTimeline->setChecked(true); connect(showTimeline, &QAction::triggered, this, &MainWindow::slotShowTimeline); guiActions->addAction(showTimeline->text(), showTimeline); actionCollection()->addAction(showTimeline->text(), showTimeline); QList docks = findChildren(); for (int j = 0; j < docks.count(); ++j) { QDockWidget *dock = docks.at(j); QAction *dockInformations = dock->toggleViewAction(); if (!dockInformations) { continue; } dockInformations->setChecked(!dock->isHidden()); guiActions->addAction(dockInformations->text(), dockInformations); } kdenliveCategoryMap.insert(QStringLiteral("interface"), guiActions); } QList MainWindow::getExtraActions(const QString &name) { if (!kdenliveCategoryMap.contains(name)) { return QList(); } return kdenliveCategoryMap.value(name)->actions(); } void MainWindow::slotTranscode(const QStringList &urls) { + // TODO refac : remove or reimplement transcoding + /* QString params; QString desc; if (urls.isEmpty()) { QAction *action = qobject_cast(sender()); QStringList transList = action->data().toStringList(); pCore->bin()->startClipJob(transList); return; } if (urls.isEmpty()) { m_messageLabel->setMessage(i18n("No clip to transcode"), ErrorMessage); return; } qCDebug(KDENLIVE_LOG) << "// TRANSODING FOLDER: " << pCore->bin()->getFolderInfo(); ClipTranscode *d = new ClipTranscode(urls, params, QStringList(), desc, pCore->bin()->getFolderInfo()); connect(d, &ClipTranscode::addClip, this, &MainWindow::slotAddProjectClip); d->show(); + */ } void MainWindow::slotTranscodeClip() { + // TODO refac : remove or reimplement transcoding + /* QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); const QString dialogFilter = i18n("All Supported Files") + QLatin1Char('(') + allExtensions + QStringLiteral(");;") + i18n("All Files") + QStringLiteral("(*)"); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QStringList urls = QFileDialog::getOpenFileNames(this, i18n("Files to transcode"), clipFolder, dialogFilter); if (urls.isEmpty()) { return; } slotTranscode(urls); + */ } void MainWindow::slotSetDocumentRenderProfile(const QMap &props) { KdenliveDoc *project = pCore->currentDoc(); bool modified = false; QMapIterator i(props); while (i.hasNext()) { i.next(); if (project->getDocumentProperty(i.key()) == i.value()) { continue; } project->setDocumentProperty(i.key(), i.value()); modified = true; } if (modified) { project->setModified(); } } void MainWindow::slotPrepareRendering(bool scriptExport, bool zoneOnly, const QString &chapterFile, QString scriptPath) { KdenliveDoc *project = pCore->currentDoc(); if (m_renderWidget == nullptr) { return; } QString playlistPath; QString mltSuffix(QStringLiteral(".mlt")); QList playlistPaths; QList trackNames; int tracksCount = 1; bool stemExport = m_renderWidget->isStemAudioExportEnabled(); if (scriptExport) { // QString scriptsFolder = project->projectFolder().path(QUrl::AddTrailingSlash) + "scripts/"; if (scriptPath.isEmpty()) { QString path = m_renderWidget->getFreeScriptName(project->url()); QPointer getUrl = new KUrlRequesterDialog(QUrl::fromLocalFile(path), i18n("Create Render Script"), this); getUrl->urlRequester()->setMode(KFile::File); if (getUrl->exec() == QDialog::Rejected) { delete getUrl; return; } scriptPath = getUrl->selectedUrl().toLocalFile(); delete getUrl; } QFile f(scriptPath); if (f.exists()) { if (KMessageBox::warningYesNo(this, i18n("Script file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } playlistPath = scriptPath; } else { QTemporaryFile temp(QDir::tempPath() + QStringLiteral("/kdenlive_rendering_XXXXXX.mlt")); temp.setAutoRemove(false); temp.open(); playlistPath = temp.fileName(); } int in = 0; int out; if (zoneOnly) { in = getMainTimeline()->controller()->zoneIn(); out = getMainTimeline()->controller()->zoneOut(); } else { out = (int)GenTime(project->projectDuration()).frames(pCore->getCurrentFps()) - 2; } QString playlistContent = pCore->projectManager()->projectSceneList(project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); if (!chapterFile.isEmpty()) { QDomDocument doc; QDomElement chapters = doc.createElement(QStringLiteral("chapters")); chapters.setAttribute(QStringLiteral("fps"), pCore->getCurrentFps()); doc.appendChild(chapters); const QList guidesList = project->getGuideModel()->getAllMarkers(); for (int i = 0; i < guidesList.count(); i++) { CommentedTime c = guidesList.at(i); int time = c.time().frames(pCore->getCurrentFps()); if (time >= in && time < out) { if (zoneOnly) { time = time - in; } } QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.appendChild(chapter); chapter.setAttribute(QStringLiteral("title"), c.comment()); chapter.setAttribute(QStringLiteral("time"), time); } if (!chapters.childNodes().isEmpty()) { if (!project->getGuideModel()->hasMarker(out)) { // Always insert a guide in pos 0 QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.insertBefore(chapter, QDomNode()); chapter.setAttribute(QStringLiteral("title"), i18nc("the first in a list of chapters", "Start")); chapter.setAttribute(QStringLiteral("time"), QStringLiteral("0")); } // save chapters file QFile file(chapterFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } else { file.write(doc.toString().toUtf8()); if (file.error() != QFile::NoError) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } file.close(); } } } // check if audio export is selected bool exportAudio; if (m_renderWidget->automaticAudioExport()) { - //TODO check if projact contains audio - //exportAudio = pCore->projectManager()->currentTimeline()->checkProjectAudio(); + // TODO check if projact contains audio + // exportAudio = pCore->projectManager()->currentTimeline()->checkProjectAudio(); exportAudio = true; } else { exportAudio = m_renderWidget->selectedAudioExport(); } // Set playlist audio volume to 100% QDomDocument doc; doc.setContent(playlistContent); QDomElement tractor = doc.documentElement().firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } // Add autoclose to playlists. QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.length(); ++i) { playlists.item(i).toElement().setAttribute(QStringLiteral("autoclose"), 1); } // Do we want proxy rendering if (project->useProxy() && !m_renderWidget->proxyRendering()) { QString root = doc.documentElement().attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } // replace proxy clips with originals // TODO QMap proxies = pCore->binController()->getProxies(pCore->currentDoc()->documentRoot()); QDomNodeList producers = doc.elementsByTagName(QStringLiteral("producer")); QString producerResource; QString producerService; QString suffix; QString prefix; for (int n = 0; n < producers.length(); ++n) { QDomElement e = producers.item(n).toElement(); producerResource = EffectsList::property(e, QStringLiteral("resource")); producerService = EffectsList::property(e, QStringLiteral("mlt_service")); if (producerResource.isEmpty() || producerService == QLatin1String("color")) { continue; } if (producerService == QLatin1String("timewarp")) { // slowmotion producer prefix = producerResource.section(QLatin1Char(':'), 0, 0) + QLatin1Char(':'); producerResource = producerResource.section(QLatin1Char(':'), 1); } else { prefix.clear(); } if (producerService == QLatin1String("framebuffer")) { // slowmotion producer suffix = QLatin1Char('?') + producerResource.section(QLatin1Char('?'), 1); producerResource = producerResource.section(QLatin1Char('?'), 0, 0); } else { suffix.clear(); } if (!producerResource.isEmpty()) { if (QFileInfo(producerResource).isRelative()) { producerResource.prepend(root); } if (proxies.contains(producerResource)) { QString replacementResource = proxies.value(producerResource); EffectsList::setProperty(e, QStringLiteral("resource"), prefix + replacementResource + suffix); if (producerService == QLatin1String("timewarp")) { EffectsList::setProperty(e, QStringLiteral("warp_resource"), replacementResource); } // We need to delete the "aspect_ratio" property because proxy clips // sometimes have different ratio than original clips EffectsList::removeProperty(e, QStringLiteral("aspect_ratio")); EffectsList::removeMetaProperties(e); } } } } QList docList; // check which audio tracks have to be exported if (stemExport) { - //TODO refac + // TODO refac /* //TODO port to new timeline model Timeline *ct = pCore->projectManager()->currentTimeline(); int allTracksCount = ct->tracksCount(); // reset tracks count (tracks to be rendered) tracksCount = 0; // begin with track 1 (track zero is a hidden black track) for (int i = 1; i < allTracksCount; i++) { Track *track = ct->track(i); // add only tracks to render list that are not muted and have audio if ((track != nullptr) && !track->info().isMute && track->hasAudio()) { QDomDocument docCopy = doc.cloneNode(true).toDocument(); QString trackName = track->info().trackName; // save track name trackNames << trackName; qCDebug(KDENLIVE_LOG) << "Track-Name: " << trackName; // create stem export doc content QDomNodeList tracks = docCopy.elementsByTagName(QStringLiteral("track")); for (int j = 0; j < allTracksCount; j++) { if (j != i) { // mute other tracks tracks.at(j).toElement().setAttribute(QStringLiteral("hide"), QStringLiteral("both")); } } docList << docCopy; tracksCount++; } } */ } else { docList << doc; } // create full playlistPaths for (int i = 0; i < tracksCount; i++) { QString plPath(playlistPath); // add track number to path name if (stemExport) { plPath = plPath + QLatin1Char('_') + QString(trackNames.at(i)).replace(QLatin1Char(' '), QLatin1Char('_')); } // add mlt suffix if (!plPath.endsWith(mltSuffix)) { plPath += mltSuffix; } playlistPaths << plPath; qCDebug(KDENLIVE_LOG) << "playlistPath: " << plPath << endl; // Do save scenelist QFile file(plPath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { m_messageLabel->setMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); return; } file.write(docList.at(i).toString().toUtf8()); if (file.error() != QFile::NoError) { m_messageLabel->setMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); file.close(); return; } file.close(); } m_renderWidget->slotExport(scriptExport, in, out, project->metadata(), playlistPaths, trackNames, scriptPath, exportAudio); } void MainWindow::slotUpdateTimecodeFormat(int ix) { KdenliveSettings::setFrametimecode(ix == 1); m_clipMonitor->updateTimecodeFormat(); m_projectMonitor->updateTimecodeFormat(); // TODO refac: reimplement ? // m_effectStack->transitionConfig()->updateTimecodeFormat(); // m_effectStack->updateTimecodeFormat(); pCore->bin()->updateTimecodeFormat(); getMainTimeline()->controller()->frameFormatChanged(); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); } void MainWindow::slotRemoveFocus() { getMainTimeline()->setFocus(); } void MainWindow::slotShutdown() { pCore->currentDoc()->setModified(false); // Call shutdown QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver"))) { QDBusInterface smserver(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QStringLiteral("org.kde.KSMServerInterface")); smserver.call(QStringLiteral("logout"), 1, 2, 2); } else if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager"))) { QDBusInterface smserver(QStringLiteral("org.gnome.SessionManager"), QStringLiteral("/org/gnome/SessionManager"), QStringLiteral("org.gnome.SessionManager")); smserver.call(QStringLiteral("Shutdown")); } } void MainWindow::slotSwitchMonitors() { pCore->monitorManager()->slotSwitchMonitors(!m_clipMonitor->isActive()); if (m_projectMonitor->isActive()) { getMainTimeline()->setFocus(); } else { pCore->bin()->focusBinView(); } } void MainWindow::slotSwitchMonitorOverlay(QAction *action) { if (pCore->monitorManager()->isActive(Kdenlive::ClipMonitor)) { m_clipMonitor->switchMonitorInfo(action->data().toInt()); } else { m_projectMonitor->switchMonitorInfo(action->data().toInt()); } } void MainWindow::slotSwitchDropFrames(bool drop) { m_clipMonitor->switchDropFrames(drop); m_projectMonitor->switchDropFrames(drop); } void MainWindow::slotSetMonitorGamma(int gamma) { KdenliveSettings::setMonitor_gamma(gamma); m_clipMonitor->updateMonitorGamma(); m_projectMonitor->updateMonitorGamma(); } void MainWindow::slotInsertZoneToTree() { if (!m_clipMonitor->isActive() || m_clipMonitor->currentController() == nullptr) { return; } QPoint info = m_clipMonitor->getZoneInfo(); - pCore->bin()->slotAddClipCut(m_clipMonitor->activeClipId(), info.x(), info.y()); + QString id; + pCore->projectItemModel()->requestAddBinSubClip(id, info.x(), info.y(), m_clipMonitor->activeClipId()); } void MainWindow::slotInsertZoneToTimeline() { QPoint info = m_clipMonitor->getZoneInfo(); QString clipData = QString("%1#%2#%3").arg(m_clipMonitor->activeClipId()).arg(info.x()).arg(info.y()); int cid = getMainTimeline()->controller()->insertClip(-1, -1, clipData, true, true); if (cid == -1) { pCore->displayMessage(i18n("Cannot insert clip at requested position"), InformationMessage); } else { getMainTimeline()->controller()->seekToClip(cid, true); } } void MainWindow::slotMonitorRequestRenderFrame(bool request) { if (request) { m_projectMonitor->sendFrameForAnalysis(true); return; } for (int i = 0; i < m_gfxScopesList.count(); ++i) { if (m_gfxScopesList.at(i)->isVisible() && tabifiedDockWidgets(m_gfxScopesList.at(i)).isEmpty() && static_cast(m_gfxScopesList.at(i)->widget())->autoRefreshEnabled()) { request = true; break; } } #ifdef DEBUG_MAINW qCDebug(KDENLIVE_LOG) << "Any scope accepting new frames? " << request; #endif if (!request) { m_projectMonitor->sendFrameForAnalysis(false); } } void MainWindow::slotUpdateProxySettings() { KdenliveDoc *project = pCore->currentDoc(); if (m_renderWidget) { m_renderWidget->updateProxyConfig(project->useProxy()); } pCore->bin()->refreshProxySettings(); } void MainWindow::slotArchiveProject() { QList> list = pCore->binController()->getControllerList(); KdenliveDoc *doc = pCore->currentDoc(); // TODO refac /* pCore->binController()->saveDocumentProperties(pCore->projectManager()->currentTimeline()->documentProperties(), doc->metadata(), doc->getGuideModel()); QDomDocument xmlDoc = doc->xmlSceneList(m_projectMonitor->sceneList(doc->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile())); QScopedPointer d( new ArchiveWidget(doc->url().fileName(), xmlDoc, list, pCore->projectManager()->currentTimeline()->projectView()->extractTransitionsLumas(), this)); if (d->exec() != 0) { m_messageLabel->setMessage(i18n("Archiving project"), OperationCompletedMessage); } */ } void MainWindow::slotDownloadResources() { QString currentFolder; if (pCore->currentDoc()) { currentFolder = pCore->currentDoc()->projectDataFolder(); } else { currentFolder = KdenliveSettings::defaultprojectfolder(); } auto *d = new ResourceWidget(currentFolder); connect(d, SIGNAL(addClip(QUrl, QStringList)), this, SLOT(slotAddProjectClip(QUrl, QStringList))); d->show(); } void MainWindow::slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes) { if (type == AVWidget) { // This data should be sent to the effect stack // TODO REFAC reimplement // m_effectStack->setKeyframes(tag, data); } else if (type == TransitionWidget) { // This data should be sent to the transition stack // TODO REFAC reimplement // m_effectStack->transitionConfig()->setKeyframes(tag, data); } else { // Error } } void MainWindow::slotAlignPlayheadToMousePos() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->seekToMouse(); } void MainWindow::triggerKey(QKeyEvent *ev) { // Hack: The QQuickWindow that displays fullscreen monitor does not integrate quith QActions. // so on keypress events we parse keys and check for shortcuts in all existing actions QKeySequence seq; // Remove the Num modifier or some shortcuts like "*" will not work if (ev->modifiers() != Qt::KeypadModifier) { seq = QKeySequence(ev->key() + static_cast(ev->modifiers())); } else { seq = QKeySequence(ev->key()); } QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { if (tempAction->shortcuts().contains(seq)) { // Trigger action tempAction->trigger(); ev->accept(); return; } } } } QDockWidget *MainWindow::addDock(const QString &title, const QString &objectName, QWidget *widget, Qt::DockWidgetArea area) { QDockWidget *dockWidget = new QDockWidget(title, this); dockWidget->setObjectName(objectName); dockWidget->setWidget(widget); addDockWidget(area, dockWidget); connect(dockWidget, &QDockWidget::dockLocationChanged, this, &MainWindow::updateDockTitleBars); connect(dockWidget, &QDockWidget::topLevelChanged, this, &MainWindow::updateDockTitleBars); return dockWidget; } void MainWindow::slotUpdateMonitorOverlays(int id, int code) { QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (!monitorOverlay) { return; } QList actions = monitorOverlay->actions(); for (QAction *ac : actions) { int mid = ac->data().toInt(); if (mid == 0x010) { ac->setEnabled(id == Kdenlive::ClipMonitor); } ac->setChecked(code & mid); } } void MainWindow::slotChangeStyle(QAction *a) { QString style = a->data().toString(); KdenliveSettings::setWidgetstyle(style); doChangeStyle(); } void MainWindow::doChangeStyle() { QString newStyle = KdenliveSettings::widgetstyle(); if (newStyle.isEmpty() || newStyle == QStringLiteral("Default")) { newStyle = defaultStyle("Breeze"); } QApplication::setStyle(QStyleFactory::create(newStyle)); // Changing widget style resets color theme, so update color theme again ThemeManager::instance()->slotChangePalette(); } bool MainWindow::isTabbedWith(QDockWidget *widget, const QString &otherWidget) { QList tabbed = tabifiedDockWidgets(widget); for (int i = 0; i < tabbed.count(); i++) { if (tabbed.at(i)->objectName() == otherWidget) { return true; } } return false; } void MainWindow::updateDockTitleBars(bool isTopLevel) { if (!KdenliveSettings::showtitlebars() || !isTopLevel) { return; } #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) QList docks = pCore->window()->findChildren(); for (int i = 0; i < docks.count(); ++i) { QDockWidget *dock = docks.at(i); QWidget *bar = dock->titleBarWidget(); if (dock->isFloating()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } QList docked = pCore->window()->tabifiedDockWidgets(dock); if (docked.isEmpty()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } bool hasVisibleDockSibling = false; for (QDockWidget *sub : docked) { if (sub->toggleViewAction()->isChecked()) { // we have another docked widget, so tabs are visible and can be used instead of title bars hasVisibleDockSibling = true; break; } } if (!hasVisibleDockSibling) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } if (!bar) { dock->setTitleBarWidget(new QWidget); } } #endif } void MainWindow::slotToggleAutoPreview(bool enable) { // TODO refac /* KdenliveSettings::setAutopreview(enable); if (enable && pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->startPreviewRender(); } */ } void MainWindow::configureToolbars() { // Since our timeline toolbar is a non-standard toolbar (as it is docked in a custom widget, not // in a QToolBarDockArea, we have to hack KXmlGuiWindow to avoid a crash when saving toolbar config. // This is why we hijack the configureToolbars() and temporarily move the toolbar to a standard location QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); ctnLay->removeWidget(m_timelineToolBar); addToolBar(Qt::BottomToolBarArea, m_timelineToolBar); auto *toolBarEditor = new KEditToolBar(guiFactory(), this); toolBarEditor->setAttribute(Qt::WA_DeleteOnClose); connect(toolBarEditor, SIGNAL(newToolBarConfig()), SLOT(saveNewToolbarConfig())); connect(toolBarEditor, &QDialog::finished, this, &MainWindow::rebuildTimlineToolBar); toolBarEditor->show(); } void MainWindow::rebuildTimlineToolBar() { // Timeline toolbar settings changed, we can now re-add our toolbar to custom location m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); removeToolBar(m_timelineToolBar); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); if (ctnLay) { ctnLay->insertWidget(0, m_timelineToolBar); } m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); m_timelineToolBar->setVisible(true); } void MainWindow::showTimelineToolbarMenu(const QPoint &pos) { QMenu menu; menu.addAction(actionCollection()->action(KStandardAction::name(KStandardAction::ConfigureToolbars))); QMenu *contextSize = new QMenu(i18n("Icon Size")); menu.addMenu(contextSize); auto *sizeGroup = new QActionGroup(contextSize); int currentSize = m_timelineToolBar->iconSize().width(); QAction *a = new QAction(i18nc("@item:inmenu Icon size", "Default"), contextSize); a->setData(m_timelineToolBar->iconSizeDefault()); a->setCheckable(true); if (m_timelineToolBar->iconSizeDefault() == currentSize) { a->setChecked(true); } a->setActionGroup(sizeGroup); contextSize->addAction(a); KIconTheme *theme = KIconLoader::global()->theme(); QList avSizes; if (theme) { avSizes = theme->querySizes(KIconLoader::Toolbar); } qSort(avSizes); if (avSizes.count() < 10) { // Fixed or threshold type icons Q_FOREACH (int it, avSizes) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); } } else { // Scalable icons. const int progression[] = {16, 22, 32, 48, 64, 96, 128, 192, 256}; for (uint i = 0; i < 9; i++) { Q_FOREACH (int it, avSizes) { if (it >= progression[i]) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); break; } } } } connect(contextSize, &QMenu::triggered, this, &MainWindow::setTimelineToolbarIconSize); menu.exec(m_timelineToolBar->mapToGlobal(pos)); contextSize->deleteLater(); } void MainWindow::setTimelineToolbarIconSize(QAction *a) { if (!a) { return; } int size = a->data().toInt(); m_timelineToolBar->setIconDimensions(size); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->saveSettings(tbGroup); } void MainWindow::slotManageCache() { QDialog d(this); d.setWindowTitle(i18n("Manage Cache Data")); auto *lay = new QVBoxLayout; TemporaryData tmp(pCore->currentDoc(), false, this); connect(&tmp, &TemporaryData::disableProxies, this, &MainWindow::slotDisableProxies); // TODO refac /* connect(&tmp, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); */ QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject); lay->addWidget(&tmp); lay->addWidget(buttonBox); d.setLayout(lay); d.exec(); } void MainWindow::slotUpdateCompositing(QAction *compose) { int mode = compose->data().toInt(); getMainTimeline()->controller()->switchCompositing(mode); if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::slotUpdateCompositeAction(int mode) { QList actions = m_compositeAction->actions(); for (int i = 0; i < actions.count(); i++) { if (actions.at(i)->data().toInt() == mode) { m_compositeAction->setCurrentAction(actions.at(i)); break; } } if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::showMenuBar(bool show) { if (!show) { KMessageBox::information(this, i18n("This will hide the menu bar completely. You can show it again by typing Ctrl+M."), i18n("Hide menu bar"), QStringLiteral("show-menubar-warning")); } menuBar()->setVisible(show); } void MainWindow::forceIconSet(bool force) { KdenliveSettings::setForce_breeze(force); if (force) { // Check current color theme QColor background = qApp->palette().window().color(); bool useDarkIcons = background.value() < 100; KdenliveSettings::setUse_dark_breeze(useDarkIcons); } if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive needs to be restarted to apply icon theme change. Restart now ?")) == KMessageBox::Continue) { slotRestart(); } } void MainWindow::slotSwitchTrimMode() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->switchTrimMode(); } */ } void MainWindow::setTrimMode(const QString &mode) { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { m_trimLabel->setText(mode); m_trimLabel->setVisible(!mode.isEmpty()); } */ } TimelineWidget *MainWindow::getMainTimeline() const { return m_timelineTabs->getMainTimeline(); } TimelineWidget *MainWindow::getCurrentTimeline() const { return m_timelineTabs->getCurrentTimeline(); } void MainWindow::slotChangeSpeed(int speed) { ObjectId owner = m_assetPanel->effectStackOwner(); - //TODO: manage bin clips / tracks + // TODO: manage bin clips / tracks if (owner.first == ObjectType::TimelineClip) { getCurrentTimeline()->controller()->changeItemSpeed(owner.second, speed); } } #ifdef DEBUG_MAINW #undef DEBUG_MAINW #endif diff --git a/src/mltcontroller/bincontroller.cpp b/src/mltcontroller/bincontroller.cpp index 3fb479f5d..3fe34eca6 100644 --- a/src/mltcontroller/bincontroller.cpp +++ b/src/mltcontroller/bincontroller.cpp @@ -1,537 +1,544 @@ /*************************************************************************** * Copyright (C) 2014 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 "bincontroller.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectitemmodel.h" #include "clip.h" #include "clipcontroller.h" #include "core.h" #include "kdenlivesettings.h" static const char *kPlaylistTrackId = "main bin"; BinController::BinController(const QString &profileName) : QObject() { // resetProfile(profileName.isEmpty() ? KdenliveSettings::current_profile() : profileName); } BinController::~BinController() { qDebug() << "/// delete bincontroller"; qDebug()<<"REMAINING CLIPS: "<clear(); } qDeleteAll(m_extraClipList); m_extraClipList.clear(); m_clipList.clear(); } void BinController::loadExtraProducer(const QString &id, Mlt::Producer *prod) { if (m_extraClipList.contains(id)) { return; } m_extraClipList.insert(id, prod); } QStringList BinController::getProjectHashes() { QStringList hashes; QMapIterator> i(m_clipList); hashes.reserve(m_clipList.count()); while (i.hasNext()) { i.next(); hashes << i.value()->getClipHash(); } hashes.removeDuplicates(); return hashes; } void BinController::initializeBin(Mlt::Playlist playlist) { qDebug() << "init bin"; // Load folders Mlt::Properties folderProperties; Mlt::Properties playlistProps(playlist.get_properties()); folderProperties.pass_values(playlistProps, "kdenlive:folder."); pCore->projectItemModel()->loadFolders(folderProperties); // Read notes QString notes = playlistProps.get("kdenlive:documentnotes"); emit setDocumentNotes(notes); // Fill Controller's list m_binPlaylist.reset(new Mlt::Playlist(playlist)); m_binPlaylist->set("id", kPlaylistTrackId); qDebug() << "Found " << m_binPlaylist->count() << "clips"; int max = m_binPlaylist->count(); for (int i = 0; i < max; i++) { QScopedPointer prod(m_binPlaylist->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")); qDebug() << "clip id" << id; if (id.contains(QLatin1Char('_'))) { // 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 { // Controller was already added by a track producer, add master now if (m_clipList.contains(id)) { m_clipList[id]->addMasterProducer(producer); } else { // Controller has not been created yet // fix MLT somehow adding root to color producer's resource (report upstream) if (strcmp(producer->get("mlt_service"), "color") == 0) { QString color = producer->get("resource"); if (color.contains(QLatin1Char('/'))) { color = color.section(QLatin1Char('/'), -1, -1); producer->set("resource", color.toUtf8().constData()); } } requestClipInfo info; info.imageHeight = 0; info.clipId = id; info.replaceProducer = false; emit slotProducerReady(info, producer); } } emit loadingBin(i + 1); } } // TODO REFACTOR: DELETE void BinController::createIfNeeded(Mlt::Profile *profile) { if (m_binPlaylist.get()) { return; } m_binPlaylist.reset(new Mlt::Playlist(*profile)); m_binPlaylist->set("id", kPlaylistTrackId); } void BinController::loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor) { destroyBin(); Mlt::Properties retainList((mlt_properties)documentTractor->get_data("xml_retain")); qDebug() << "Loading bin playlist..."; if (retainList.is_valid() && (retainList.get_data(binPlaylistId().toUtf8().constData()) != nullptr)) { Mlt::Playlist playlist((mlt_playlist)retainList.get_data(binPlaylistId().toUtf8().constData())); qDebug() << "retain is valid"; if (playlist.is_valid() && playlist.type() == playlist_type) { qDebug() << "playlist is valid"; // Load bin clips initializeBin(playlist); } } // If no Playlist found, create new one if (!m_binPlaylist) { qDebug() << "no playlist valid, creating"; m_binPlaylist.reset(new Mlt::Playlist(*modelTractor->profile())); m_binPlaylist->set("id", kPlaylistTrackId); } QString retain = QStringLiteral("xml_retain %1").arg(binPlaylistId()); modelTractor->set(retain.toUtf8().constData(), m_binPlaylist->get_service(), 0); } void BinController::slotStoreFolder(const QString &folderId, const QString &parentId, const QString &oldParentId, const QString &folderName) { if (!oldParentId.isEmpty()) { // Folder was moved, remove old reference QString oldPropertyName = "kdenlive:folder." + oldParentId + QLatin1Char('.') + folderId; m_binPlaylist->set(oldPropertyName.toUtf8().constData(), (char *)nullptr); } QString propertyName = "kdenlive:folder." + parentId + QLatin1Char('.') + folderId; if (folderName.isEmpty()) { // Remove this folder info m_binPlaylist->set(propertyName.toUtf8().constData(), (char *)nullptr); } else { m_binPlaylist->set(propertyName.toUtf8().constData(), folderName.toUtf8().constData()); } } void BinController::storeMarker(const QString &markerId, const QString &markerHash) { QString propertyName = "kdenlive:marker." + markerId; if (markerHash.isEmpty()) { // Remove this marker m_binPlaylist->set(propertyName.toUtf8().constData(), (char *)nullptr); } else { m_binPlaylist->set(propertyName.toUtf8().constData(), markerHash.toUtf8().constData()); } } mlt_service BinController::service() { return m_binPlaylist->get_service(); } const QString BinController::binPlaylistId() { return kPlaylistTrackId; } int BinController::clipCount() const { return m_clipList.size(); } void BinController::addClipToBin(const QString &id, const std::shared_ptr &controller, bool fromPlaylist) { /** Test: we can use filters on clips in the bin this way Mlt::Filter f(*m_mltProfile, "sepia"); producer.attach(f); */ // append or replace clip in MLT's retain playlist if (!fromPlaylist && controller->isValid()) { replaceBinPlaylistClip(id, controller->originalProducer()); } if (!m_clipList.contains(id)) { m_clipList.insert(id, controller); } } void BinController::replaceBinPlaylistClip(const QString &id, const std::shared_ptr &producer) { removeBinPlaylistClip(id); m_binPlaylist->append(*producer.get()); } void BinController::pasteEffects(const QString &id, const std::shared_ptr &producer) { std::shared_ptr ctrl = getController(id); if (ctrl) { duplicateFilters(ctrl->originalProducer(), *producer.get()); } } void BinController::removeBinPlaylistClip(const QString &id) { int size = m_binPlaylist->count(); for (int i = 0; i < size; i++) { QScopedPointer prod(m_binPlaylist->get_clip(i)); QString prodId = prod->parent().get("kdenlive:id"); if (prodId == id) { m_binPlaylist->remove(i); break; } } } bool BinController::hasClip(const QString &id) { return m_clipList.contains(id); } bool BinController::removeBinClip(const QString &id) { if (!m_clipList.contains(id)) { return false; } removeBinPlaylistClip(id); m_clipList.remove(id); return true; } Mlt::Producer *BinController::cloneProducer(Mlt::Producer &original) { Clip clp(original); Mlt::Producer *clone = clp.clone(); return clone; } std::shared_ptr BinController::getBinProducer(const QString &id) { // TODO: framebuffer speed clips if (!m_clipList.contains(id)) { qDebug() << "ERROR: requesting invalid bin producer: "<originalProducer(); } Mlt::Producer *BinController::getBinVideoProducer(const QString &id) { QString videoId = id + QStringLiteral("_video"); if (!m_extraClipList.contains(videoId)) { // create clone QString originalId = id.section(QLatin1Char('_'), 0, 0); std::shared_ptr original = getBinProducer(originalId); Mlt::Producer *videoOnly = cloneProducer(*original.get()); videoOnly->set("audio_index", -1); videoOnly->set("kdenlive:id", videoId.toUtf8().constData()); m_extraClipList.insert(videoId, videoOnly); return videoOnly; } return m_extraClipList.value(videoId); } void BinController::duplicateFilters(std::shared_ptr original, Mlt::Producer clone) { Mlt::Service clipService(original->get_service()); Mlt::Service dupService(clone.get_service()); for (int ix = 0; ix < clipService.filter_count(); ++ix) { QScopedPointer filter(clipService.filter(ix)); // Only duplicate Kdenlive filters if (filter->is_valid()) { QString effectId = filter->get("kdenlive_id"); if (effectId.isEmpty()) { continue; } // looks like there is no easy way to duplicate a filter, // so we will create a new one and duplicate its properties auto *dup = new Mlt::Filter(*original->profile(), filter->get("mlt_service")); if ((dup != nullptr) && dup->is_valid()) { for (int i = 0; i < filter->count(); ++i) { QString paramName = filter->get_name(i); if (paramName.at(0) != QLatin1Char('_')) { dup->set(filter->get_name(i), filter->get(i)); } } dupService.attach(*dup); } delete dup; } } } QStringList BinController::getClipIds() const { return m_clipList.keys(); } QString BinController::xmlFromId(const QString &id) { if (!m_clipList.contains(id)) { qDebug() << "Error: impossible to retrieve xml from unknown bin clip"; return QString(); } std::shared_ptr controller = m_clipList[id]; std::shared_ptr original = controller->originalProducer(); QString xml = getProducerXML(original); QDomDocument mltData; mltData.setContent(xml); QDomElement producer = mltData.documentElement().firstChildElement(QStringLiteral("producer")); QString str; QTextStream stream(&str); producer.save(stream, 4); return str; } // static QString BinController::getProducerXML(const std::shared_ptr &producer, bool includeMeta) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); if (!s.is_valid()) { return QString(); } int ignore = s.get_int("ignore_points"); if (ignore != 0) { s.set("ignore_points", 0); } c.set("time_format", "frames"); if (!includeMeta) { c.set("no_meta", 1); } c.set("store", "kdenlive"); c.set("no_root", 1); c.set("root", "/"); c.connect(s); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return QString::fromUtf8(c.get("string")); } std::shared_ptr BinController::getController(const QString &id) { if (!m_clipList.contains(id)) { qDebug() << "Error: invalid bin clip requested" << id; Q_ASSERT(false); } return m_clipList.value(id); } const QList> BinController::getControllerList() const { return m_clipList.values(); } const QStringList BinController::getBinIdsByResource(const QFileInfo &url) const { QStringList controllers; QMapIterator> i(m_clipList); while (i.hasNext()) { i.next(); auto ctrl = i.value(); if (QFileInfo(ctrl->clipUrl()) == url) { controllers << i.key(); } } return controllers; } void BinController::updateTrackProducer(const QString &id) { emit updateTimelineProducer(id); } + void BinController::checkThumbnails(const QDir &thumbFolder) { + // TODO refac: this has to use the new thumb job + /* // Parse all controllers and load thumbnails QMapIterator> i(m_clipList); while (i.hasNext()) { i.next(); std::shared_ptr ctrl = i.value(); if (ctrl->clipType() == Audio) { // no thumbnails for audio clip continue; } bool foundFile = false; if (!ctrl->getClipHash().isEmpty()) { QImage img(thumbFolder.absoluteFilePath(ctrl->getClipHash() + QStringLiteral(".png"))); if (!img.isNull()) { emit loadThumb(ctrl->binId(), img, true); foundFile = true; } } if (!foundFile) { // Add clip id to thumbnail generation thread QDomDocument doc; ctrl->getProducerXML(doc); QDomElement xml = doc.documentElement().firstChildElement(QStringLiteral("producer")); if (!xml.isNull()) { xml.setAttribute(QStringLiteral("thumbnailOnly"), 1); emit createThumb(xml, ctrl->binId(), 150); } } } + */ } void BinController::checkAudioThumbs() { + // TODO refac: this has to use the new audio thumb job + /* QMapIterator> i(m_clipList); while (i.hasNext()) { i.next(); std::shared_ptr ctrl = i.value(); if (!ctrl->m_audioThumbCreated) { if (KdenliveSettings::audiothumbnails()) { // We want audio thumbnails emit requestAudioThumb(ctrl->binId()); } else { // Abort all pending thumb creation emit abortAudioThumbs(); break; } } } + */ } void BinController::saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel) { // Clear previous properites Mlt::Properties playlistProps(m_binPlaylist->get_properties()); Mlt::Properties docProperties; docProperties.pass_values(playlistProps, "kdenlive:docproperties."); for (int i = 0; i < docProperties.count(); i++) { QString propName = QStringLiteral("kdenlive:docproperties.") + docProperties.get_name(i); playlistProps.set(propName.toUtf8().constData(), (char *)nullptr); } // Clear previous metadata Mlt::Properties docMetadata; docMetadata.pass_values(playlistProps, "kdenlive:docmetadata."); for (int i = 0; i < docMetadata.count(); i++) { QString propName = QStringLiteral("kdenlive:docmetadata.") + docMetadata.get_name(i); playlistProps.set(propName.toUtf8().constData(), (char *)nullptr); } QMapIterator i(props); while (i.hasNext()) { i.next(); playlistProps.set(("kdenlive:docproperties." + i.key()).toUtf8().constData(), i.value().toUtf8().constData()); } QMapIterator j(metadata); while (j.hasNext()) { j.next(); playlistProps.set(("kdenlive:docmetadata." + j.key()).toUtf8().constData(), j.value().toUtf8().constData()); } } void BinController::saveProperty(const QString &name, const QString &value) { m_binPlaylist->set(name.toUtf8().constData(), value.toUtf8().constData()); } const QString BinController::getProperty(const QString &name) { return QString(m_binPlaylist->get(name.toUtf8().constData())); } QMap BinController::getProxies(const QString &root) { QMap proxies; int size = m_binPlaylist->count(); for (int i = 0; i < size; i++) { QScopedPointer prod(m_binPlaylist->get_clip(i)); if (!prod->is_valid() || prod->is_blank()) { continue; } QString proxy = prod->parent().get("kdenlive:proxy"); if (proxy.length() > 2) { if (QFileInfo(proxy).isRelative()) { proxy.prepend(root); } QString sourceUrl(prod->parent().get("kdenlive:originalurl")); if (QFileInfo(sourceUrl).isRelative()) { sourceUrl.prepend(root); } proxies.insert(proxy, sourceUrl); } } return proxies; } diff --git a/src/mltcontroller/bincontroller.h b/src/mltcontroller/bincontroller.h index 66423ad7c..3235a32ab 100644 --- a/src/mltcontroller/bincontroller.h +++ b/src/mltcontroller/bincontroller.h @@ -1,202 +1,200 @@ /*************************************************************************** * Copyright (C) 2014 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 BINCONTROLLER_H #define BINCONTROLLER_H #include #include "clipcontroller.h" #include "definitions.h" #include #include #include #include class MarkerListModel; namespace Mlt { class Playlist; class Profile; } /** * @class BinController * @brief This is where MLT's project clips (the bin clips) are managed * * The project profile, used to build the monitors renderers is stored here */ class BinController : public QObject, public std::enable_shared_from_this { Q_OBJECT public: explicit BinController(const QString &profileName = QString()); virtual ~BinController(); /** @brief Returns the service for the Bin's playlist, used to make sure MLT will save it correctly in its XML. */ mlt_service service(); friend class ClipController; friend class ProjectClip; protected: /** @brief Add a new clip producer to the project. This is protected because it should be called only by the ClipController itself upon creation * @param id The clip's id * @param producer The MLT producer for this clip * */ void addClipToBin(const QString &id, const std::shared_ptr &controller, bool fromPlaylist = false); public: /** @brief Store a timeline producer in clip list for later re-use * @param id The clip's id * @param producer The MLT producer for this clip * */ void loadExtraProducer(const QString &id, Mlt::Producer *prod); /** @brief Returns the name MLT will use to store our bin's playlist */ static const QString binPlaylistId(); /** @brief Clear the bin's playlist */ void destroyBin(); /** @brief Load the Bin's main playlist from an existing tractor, and pass it to the model */ void loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor); /** @brief Initialize the bin's playlist from MLT's data * @param playlist The MLT playlist containing our bin's clips */ void initializeBin(Mlt::Playlist playlist); /** @brief If our bin's playlist does not exist, create a new one */ void createIfNeeded(Mlt::Profile *profile); /** @brief Returns true if a clip with that id is in our bin's playlist * @param id The clip's id as stored in DocClipBase */ bool hasClip(const QString &id); QStringList getClipIds() const; /** @brief Delete a clip from the bin from its id. * @param id The clip's id as stored in DocClipBase * @return true on success, false on error */ bool removeBinClip(const QString &id); /** @brief Get the MLT Producer for a given id. @param id The clip id as stored in the DocClipBase class */ // TODO? Since MLT requires 1 different producer for each track to correctly handle audio mix, // we should specify on which track the clip should be. // @param track The track on which the producer will be put. Setting a value of -1 will return the master clip contained in the bin playlist // @param clipState The state of the clip (if we need an audio only or video only producer). // @param speed If the clip has a speed effect (framebuffer producer), we indicate the speed here std::shared_ptr getBinProducer(const QString &id); /** @brief returns a video only (no audio) version of this producer */ Mlt::Producer *getBinVideoProducer(const QString &id); /** @brief Returns the clip data as rendered by MLT's XML consumer, used to duplicate a clip * @param producer The clip's original producer */ static QString getProducerXML(const std::shared_ptr &producer, bool includeMeta = false); /** @brief Returns the clip data as rendered by MLT's XML consumer * @param id The clip's original id * @returns An XML element containing the clip xml */ QString xmlFromId(const QString &id); int clipCount() const; Mlt::Producer *cloneProducer(Mlt::Producer &original); std::shared_ptr getController(const QString &id); const QList> getControllerList() const; void replaceBinPlaylistClip(const QString &id, const std::shared_ptr &producer); /** @brief Get the list of ids whose clip have the resource indicated by @param url */ const QStringList getBinIdsByResource(const QFileInfo &url) const; void storeMarker(const QString &markerId, const QString &markerHash); /** @brief A Bin clip effect was changed, update track producers */ void updateTrackProducer(const QString &id); /** @brief Load thumbnails for all producers */ void checkThumbnails(const QDir &thumbFolder); /** @brief Request audio thumbnails for all producers */ void checkAudioThumbs(); /** @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 Save a property from the main bin */ const QString getProperty(const QString &name); /** @brief Return a list of proxy / original url */ QMap getProxies(const QString &root); /** @brief Returns a list of all clips hashes. */ QStringList getProjectHashes(); public slots: /** @brief Stored a Bin Folder id / name to MLT's bin playlist. Using an empry folderName deletes the property */ void slotStoreFolder(const QString &folderId, const QString &parentId, const QString &oldParentId, const QString &folderName); private: /** @brief The MLT playlist holding our Producers */ std::unique_ptr m_binPlaylist; /** @brief The current MLT profile's filename */ QString m_activeProfile; /** @brief Can be used to copy filters from a clip to another */ void duplicateFilters(std::shared_ptr original, Mlt::Producer clone); /** @brief This list holds all producer controllers for the playlist, indexed by id */ QMap> m_clipList; /** @brief This list holds all extra controllers (slowmotion, video only, ... that are in timeline, indexed by id */ QMap m_extraClipList; /** @brief Remove a clip from MLT's special bin playlist */ void removeBinPlaylistClip(const QString &id); /** @brief Duplicate effects from stored producer */ void pasteEffects(const QString &id, const std::shared_ptr &producer); signals: - void loadThumb(const QString &, const QImage &, bool); - void createThumb(const QDomElement &, const QString &, int); void requestAudioThumb(const QString &); void abortAudioThumbs(); void setDocumentNotes(const QString &); void updateTimelineProducer(const QString &); /** @brief We want to replace a clip with another, but before we need to change clip producer id so that there is no interference*/ void prepareTimelineReplacement(const requestClipInfo &, const std::shared_ptr &); /** @brief Indicate which clip we are loading */ void loadingBin(int); void slotProducerReady(const requestClipInfo &info, std::shared_ptr producer); }; #endif diff --git a/src/mltcontroller/clip.cpp b/src/mltcontroller/clip.cpp index ea9c63b7f..c44564490 100644 --- a/src/mltcontroller/clip.cpp +++ b/src/mltcontroller/clip.cpp @@ -1,211 +1,223 @@ /* * Kdenlive timeline clip handling MLT producer * Copyright 2015 Kdenlive team * Author: Vincent Pinon * * 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 "clip.h" #include #include #include Clip::Clip(Mlt::Producer &producer) : QObject() , m_producer(producer) { } Clip::Clip(Clip &other) : QObject() { m_producer = other.producer(); } Clip::~Clip() = default; Clip &Clip::operator=(Clip &other) { m_producer = other.producer(); return *this; } Mlt::Producer &Clip::producer() { return m_producer; } void Clip::setProducer(Mlt::Producer &producer) { m_producer = producer; } void Clip::adjustEffectsLength() { int ct = 0; Mlt::Filter *filter = m_producer.filter(ct); while (filter != nullptr) { if (filter->get_int("kdenlive:sync_in_out") == 1) { filter->set_in_and_out(m_producer.get_in(), m_producer.get_out()); } ct++; delete filter; filter = m_producer.filter(ct); } } void Clip::addEffects(Mlt::Service &service, bool skipFades) { for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); // Only duplicate Kdenlive filters, and skip the fade in effects if (effect->is_valid()) { QString effectId = QString::fromLatin1(effect->get("kdenlive_id")); if (effectId.isEmpty() || (skipFades && (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")))) { continue; } // no easy filter copy: do it by hand! auto *copy = new Mlt::Filter(*effect->profile(), effect->get("mlt_service")); if ((copy != nullptr) && copy->is_valid()) { for (int i = 0; i < effect->count(); ++i) { QString paramName = QString::fromLatin1(effect->get_name(i)); if (paramName == QLatin1String("kdenlive:sync_in_out") && QString::fromLatin1(effect->get(i)) == QLatin1String("1")) { // Effect in/out must be synced with clip in/out copy->set_in_and_out(m_producer.get_in(), m_producer.get_out()); } if (paramName.at(0) != QLatin1Char('_')) { copy->set(effect->get_name(i), effect->get(i)); } } m_producer.attach(*copy); } delete copy; } } } void Clip::replaceEffects(Mlt::Service &service) { // remove effects first int ct = 0; Mlt::Filter *filter = m_producer.filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_ix")); if (!ix.isEmpty()) { if (m_producer.detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = m_producer.filter(ct); } addEffects(service); } void Clip::deleteEffects() { // remove effects int ct = 0; Mlt::Filter *filter = m_producer.filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (m_producer.detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = m_producer.filter(ct); } } void Clip::disableEffects(bool disable) { int ct = 0; Mlt::Filter *filter = m_producer.filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_ix")); if (!ix.isEmpty()) { if (disable && filter->get_int("disable") == 0) { filter->set("disable", 1); filter->set("auto_disable", 1); } else if (!disable && filter->get_int("auto_disable") == 1) { filter->set("disable", (char *)nullptr); filter->set("auto_disable", (char *)nullptr); } } ct++; delete filter; filter = m_producer.filter(ct); } } const QByteArray Clip::xml() { Mlt::Consumer c(*m_producer.profile(), "xml", "string"); Mlt::Service s(m_producer.get_service()); int ignore = s.get_int("ignore_points"); if (ignore != 0) { 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("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return c.get("string"); } +const QByteArray Clip::xml(std::shared_ptr prod) +{ + Clip clp(*prod.get()); + return clp.xml(); +} Mlt::Producer *Clip::clone() { QByteArray prodXml = xml(); // HACK: currently the MLT xml producer, when parsing a , does change the global profile accordingly. // causing crash on threaded calls. To avoid this, we discard the profile info from our xml QDomDocument doc; doc.setContent(prodXml, true); QDomNodeList profiles = doc.documentElement().elementsByTagName("profile"); if (!profiles.isEmpty()) { QDomNode profile = profiles.item(0); doc.documentElement().removeChild(profile); } Mlt::Producer *clone = new Mlt::Producer(*m_producer.profile(), "xml-string", doc.toByteArray().constData()); return clone; } +std::shared_ptr Clip::clone(std::shared_ptr prod) +{ + + Clip clp(*prod.get()); + return std::shared_ptr(clp.clone()); +} + Mlt::Producer *Clip::softClone(const char *list) { QString service = QString::fromLatin1(m_producer.get("mlt_service")); QString resource = QString::fromLatin1(m_producer.get("resource")); Mlt::Producer *clone = new Mlt::Producer(*m_producer.profile(), service.toUtf8().constData(), resource.toUtf8().constData()); Mlt::Properties original(m_producer.get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } diff --git a/src/mltcontroller/clip.h b/src/mltcontroller/clip.h index 4af55431c..e29ce3ae5 100644 --- a/src/mltcontroller/clip.h +++ b/src/mltcontroller/clip.h @@ -1,64 +1,70 @@ /* * Kdenlive timeline clip handling MLT producer * Copyright 2015 Kdenlive team * Author: Vincent Pinon * * 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 CLIP_H #define CLIP_H #include +#include #include #include + +//TODO refac: extract methods that are/shoud be static into utils and delete the rest class Clip : public QObject { Q_OBJECT Q_PROPERTY(Mlt::Producer producer READ producer WRITE setProducer) public: explicit Clip(Mlt::Producer &producer); Clip(Clip &other); ~Clip(); Clip &operator=(Clip &other); const QByteArray xml(); + /* @brief Return the xml associated to a given producer */ + static const QByteArray xml(std::shared_ptr prod); /** @brief: Clone a producer (creates a completely independent copy). */ - Mlt::Producer *clone(); + static std::shared_ptr clone(std::shared_ptr prod); + Mlt::Producer* clone(); /** @brief: Clone a producer without using xml-string producer. * When Movit is used, we must use this because xml-string crashes (probably attaches some normalizers) */ Mlt::Producer *softClone(const char *list); void deleteEffects(); void addEffects(Mlt::Service &service, bool skipFades = false); void replaceEffects(Mlt::Service &service); void delEffect(int index); /** @brief: Dis/enable all kdenlive effects on a clip. */ void disableEffects(bool disable); void adjustEffectsLength(); Mlt::Producer &producer(); public Q_SLOTS: void setProducer(Mlt::Producer &producer); private: Mlt::Producer m_producer; }; #endif // CLIP_H diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index 003f255a3..a21867d69 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,807 +1,808 @@ /* 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 "clipcontroller.h" #include "bin/model/markerlistmodel.hpp" #include "bincontroller.h" #include "doc/docundostack.hpp" +#include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/effectscontroller.h" #include "profiles/profilemodel.hpp" -#include "doc/kdenlivedoc.h" #include "core.h" #include "kdenlive_debug.h" #include #include #include #include std::shared_ptr ClipController::mediaUnavailable; ClipController::ClipController(const QString clipId, std::shared_ptr bincontroller, std::shared_ptr producer) : selectedEffectIndex(1) , m_audioThumbCreated(false) , m_masterProducer(producer) , m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr) , m_usesProxy(false) , m_audioInfo(nullptr) , m_audioIndex(0) , m_videoIndex(0) - , m_clipType(Unknown) + , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_binController(bincontroller) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_properties) { setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); m_service = m_properties->get("mlt_service"); QString proxy = m_properties->get("kdenlive:proxy"); QString path = m_properties->get("resource"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } m_path = QFileInfo(path).absoluteFilePath(); getInfoForProducer(); } else { m_producerLock.lock(); } } ClipController::~ClipController() { delete m_properties; m_masterProducer.reset(); - delete m_audioInfo; } const QString ClipController::binId() const { return m_controllerBinId; } - -AudioStreamInfo *ClipController::audioInfo() const +const std::unique_ptr &ClipController::audioInfo() const { return m_audioInfo; } void ClipController::addMasterProducer(const std::shared_ptr &producer) { + qDebug() << "################### ClipController::addmasterproducer"; QString documentRoot = pCore->currentDoc()->documentRoot(); m_masterProducer = producer; m_properties = new Mlt::Properties(m_masterProducer->get_properties()); int id = m_controllerBinId.toInt(); m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack()); if (!m_masterProducer->is_valid()) { m_masterProducer = ClipController::mediaUnavailable; m_producerLock.unlock(); qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_producerLock.unlock(); QString proxy = m_properties->get("kdenlive:proxy"); m_service = m_properties->get("mlt_service"); QString path = m_properties->get("resource"); m_usesProxy = false; if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_path = QFileInfo(path).absoluteFilePath(); getInfoForProducer(); emitProducerChanged(m_controllerBinId, producer); setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); } connectEffectStack(); } void ClipController::getProducerXML(QDomDocument &document, bool includeMeta) { + // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = BinController::getProducerXML(m_masterProducer, includeMeta); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { + qDebug() << "################### ClipController::getinfoforproducer"; date = QFileInfo(m_path).lastModified(); m_audioIndex = -1; m_videoIndex = -1; // special case: playlist with a proxy clip have to be detected separately if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) { - m_clipType = Playlist; + m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) { m_audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_audioIndex == -1) { - m_clipType = Video; + m_clipType = ClipType::Video; } else if (m_videoIndex == -1) { - m_clipType = Audio; + m_clipType = ClipType::Audio; } else { - m_clipType = AV; + m_clipType = ClipType::AV; } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { - m_clipType = SlideShow; + m_clipType = ClipType::SlideShow; } else { - m_clipType = Image; + m_clipType = ClipType::Image; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) { - m_clipType = Color; + m_clipType = ClipType::Color; m_hasLimitedDuration = false; } else if (m_service == QLatin1String("kdenlivetitle")) { if (!m_path.isEmpty()) { - m_clipType = TextTemplate; + m_clipType = ClipType::TextTemplate; } else { - m_clipType = Text; + m_clipType = ClipType::Text; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) { - m_clipType = Playlist; + m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("webvfx")) { - m_clipType = WebVfx; + m_clipType = ClipType::WebVfx; } else if (m_service == QLatin1String("qtext")) { - m_clipType = QText; + m_clipType = ClipType::QText; } else { - m_clipType = Unknown; + m_clipType = ClipType::Unknown; } - if (m_audioIndex > -1 || m_clipType == Playlist) { - m_audioInfo = new AudioStreamInfo(m_masterProducer.get(), m_audioIndex); + if (m_audioIndex > -1 || m_clipType == ClipType::Playlist) { + m_audioInfo.reset(new AudioStreamInfo(m_masterProducer.get(), m_audioIndex)); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->get_playtime()); m_masterProducer->set("out", m_masterProducer->get_length() - 1); } } } bool ClipController::hasLimitedDuration() const { return m_hasLimitedDuration; } std::shared_ptr ClipController::originalProducer() { QMutexLocker lock(&m_producerLock); return m_masterProducer; } Mlt::Producer *ClipController::masterProducer() { return new Mlt::Producer(*m_masterProducer); } bool ClipController::isValid() { if (m_masterProducer == nullptr) { return false; } return m_masterProducer->is_valid(); } // static const char *ClipController::getPassPropertiesList(bool passLength) { if (!passLength) { return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,file_hash,autorotate"; } return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length"; } QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, prefix.toUtf8().constData()); QMap subclipsData; for (int i = 0; i < subProperties.count(); i++) { subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i)); } return subclipsData; } void ClipController::updateProducer(const std::shared_ptr &producer) { + qDebug() << "################### ClipController::updateProducer"; // TODO replace all track producers if (!m_properties) { // producer has not been initialized return addMasterProducer(producer); } Mlt::Properties passProperties; // Keep track of necessary properties QString proxy = producer->get("kdenlive:proxy"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property m_usesProxy = true; } else { m_usesProxy = false; } passProperties.pass_list(*m_properties, getPassPropertiesList(m_usesProxy)); delete m_properties; *m_masterProducer = producer.get(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); // Pass properties from previous producer m_properties->pass_list(passProperties, getPassPropertiesList(m_usesProxy)); m_producerLock.unlock(); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { // URL and name shoule not be updated otherwise when proxying a clip we cannot find back the original url /*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource")); if (m_url.isValid()) { m_name = m_url.fileName(); } */ } qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource"); } Mlt::Producer *ClipController::getTrackProducer(const QString &trackName, PlaylistState::ClipState clipState, double speed) { // TODO Delete this Q_UNUSED(speed); Q_UNUSED(trackName); Q_UNUSED(clipState); return m_masterProducer.get(); /* //TODO Q_UNUSED(speed) if (trackName.isEmpty()) { return m_masterProducer; } if (m_clipType != AV && m_clipType != Audio && m_clipType != Playlist) { // Only producers with audio need a different producer for each track (or we have an audio crackle bug) return new Mlt::Producer(m_masterProducer->parent()); } QString clipWithTrackId = clipId(); clipWithTrackId.append(QLatin1Char('_') + trackName); //TODO handle audio / video only producers and framebuffer if (clipState == PlaylistState::AudioOnly) { clipWithTrackId.append(QStringLiteral("_audio")); } else if (clipState == PlaylistState::VideoOnly) { clipWithTrackId.append(QStringLiteral("_video")); } Mlt::Producer *clone = m_binController->cloneProducer(*m_masterProducer); clone->set("id", clipWithTrackId.toUtf8().constData()); //m_binController->replaceBinPlaylistClip(clipWithTrackId, clone->parent()); return clone; */ } const QString ClipController::getStringDuration() { if (m_masterProducer) { int playtime = m_masterProducer->get_int("kdenlive:duration"); if (playtime > 0) { return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df)); } return m_masterProducer->get_length_time(mlt_time_smpte_df); } return i18n("Unknown"); } GenTime ClipController::getPlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return GenTime(m_masterProducer->get_playtime(), fps); } int ClipController::getFramePlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return 0; } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); return playtime == 0 ? m_masterProducer->get_playtime() : playtime; } return m_masterProducer->get_playtime(); } QString ClipController::getProducerProperty(const QString &name) const { if (!m_properties) { return QString(); } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get(correctedName.toUtf8().constData()); } return QString(m_properties->get(name.toUtf8().constData())); } int ClipController::getProducerIntProperty(const QString &name) const { if (!m_properties) { return 0; } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get_int(correctedName.toUtf8().constData()); } return m_properties->get_int(name.toUtf8().constData()); } qint64 ClipController::getProducerInt64Property(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_int64(name.toUtf8().constData()); } double ClipController::getProducerDoubleProperty(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_double(name.toUtf8().constData()); } QColor ClipController::getProducerColorProperty(const QString &name) const { if (!m_properties) { return QColor(); } mlt_color color = m_properties->get_color(name.toUtf8().constData()); return QColor::fromRgb(color.r, color.g, color.b); } QMap ClipController::currentProperties(const QMap &props) { QMap currentProps; QMap::const_iterator i = props.constBegin(); while (i != props.constEnd()) { currentProps.insert(i.key(), getProducerProperty(i.key())); ++i; } return currentProps; } double ClipController::originalFps() const { if (!m_properties) { return 0; } QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex); return m_properties->get_double(propertyName.toUtf8().constData()); } QString ClipController::videoCodecProperty(const QString &property) const { if (!m_properties) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::codec(bool audioCodec) const { - if ((m_properties == nullptr) || (m_clipType != AV && m_clipType != Video && m_clipType != Audio)) { + if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_audioIndex : m_videoIndex); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::clipUrl() const { return m_path; } QString ClipController::clipName() const { QString name = getProducerProperty(QStringLiteral("kdenlive:clipname")); if (!name.isEmpty()) { return name; } return QFileInfo(m_path).fileName(); } QString ClipController::description() const { - if (m_clipType == TextTemplate) { + if (m_clipType == ClipType::TextTemplate) { QString name = getProducerProperty(QStringLiteral("templatetext")); return name; } QString name = getProducerProperty(QStringLiteral("kdenlive:description")); if (!name.isEmpty()) { return name; } return getProducerProperty(QStringLiteral("meta.attr.comment.markup")); } QString ClipController::serviceName() const { return m_service; } void ClipController::setProducerProperty(const QString &name, int value) { // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, double value) { // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, const QString &value) { // TODO: also set property on all track producers if (value.isEmpty()) { m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } else { m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData()); } } void ClipController::resetProducerProperty(const QString &name) { // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType ClipController::clipType() const { return m_clipType; } const QSize ClipController::getFrameSize() const { if (m_masterProducer == nullptr) { return QSize(); } int width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } int height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } return QSize(width, height); } QPixmap ClipController::pixmap(int framePosition, int width, int height) { + // TODO refac this should use the new thumb infrastructure m_masterProducer->seek(framePosition); Mlt::Frame *frame = m_masterProducer->get_frame(); if (frame == nullptr || !frame->is_valid()) { QPixmap p(width, height); p.fill(QColor(Qt::red).rgb()); return p; } frame->set("rescale.interp", "bilinear"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (width == 0) { width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } } if (height == 0) { height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } } // int ow = frameWidth; // int oh = height; mlt_image_format format = mlt_image_rgb24a; width += width % 2; height += height % 2; const uchar *imagedata = frame->get_image(format, width, height); QImage image(imagedata, width, height, QImage::Format_RGBA8888); QPixmap pixmap; pixmap.convertFromImage(image); delete frame; return pixmap; } - void ClipController::setZone(const QPoint &zone) { setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x()); setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y()); } QPoint ClipController::zone() const { int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int max = getFramePlaytime() - 1; int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max); if (out <= in) { out = max; } QPoint zone(in, out); return zone; } const QString ClipController::getClipHash() const { return getProducerProperty(QStringLiteral("kdenlive:file_hash")); } Mlt::Properties &ClipController::properties() { return *m_properties; } // TODO REFAC: this now should be managed from effectstack void ClipController::initEffect(QDomElement &xml) { QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); } void ClipController::addEffect(QDomElement &xml) { // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); // Add effect to list and setup a kdenlive_ix value int kdenlive_ix = 0; for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); int ix = effect->get_int("kdenlive_ix"); if (ix > kdenlive_ix) { kdenlive_ix = ix; } } kdenlive_ix++; xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix); EffectsParameterList params = EffectsController::getEffectArgs(xml); EffectManager effect(service); effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps())); if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::removeEffect(int effectIndex, bool delayRefresh) { // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.removeEffect(effectIndex, true); if (!delayRefresh) { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } */ } EffectsList ClipController::effectList() { return xmlEffectList(m_masterProducer->profile(), m_masterProducer->parent()); } void ClipController::moveEffect(int oldPos, int newPos) { // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.moveEffect(oldPos, newPos); */ } - void ClipController::reloadTrackProducers() { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } int ClipController::effectsCount() { int count = 0; Mlt::Service service(m_masterProducer->parent()); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QString id = effect->get("kdenlive_id"); if (!id.isEmpty()) { count++; } } return count; } // static EffectsList ClipController::xmlEffectList(Mlt::Profile *profile, Mlt::Service &service) { EffectsList effList(true); // TODO refac : rewrite this /* for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QDomElement clipeffect = Timeline::getEffectByTag(effect->get("tag"), effect->get("kdenlive_id")); QDomElement currenteffect = clipeffect.cloneNode().toElement(); // recover effect parameters QDomNodeList params = currenteffect.elementsByTagName(QStringLiteral("parameter")); if (effect->get_int("disable") == 1) { currenteffect.setAttribute(QStringLiteral("disable"), 1); } for (int i = 0; i < params.count(); ++i) { QDomElement param = params.item(i).toElement(); Timeline::setParam(param, effect->get(param.attribute(QStringLiteral("name")).toUtf8().constData())); } effList.append(currenteffect); } */ return effList; } void ClipController::changeEffectState(const QList &indexes, bool disable) { // TODO refac : this must be rewritten /* Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) { effect->set("disable", (int)disable); } } if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::updateEffect(const QDomElement &e, int ix) { // TODO refac : this must be rewritten /* QString tag = e.attribute(QStringLiteral("id")); if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) { // this filters cannot be edited, remove and re-add it removeEffect(ix, true); QDomElement clone = e.cloneNode().toElement(); addEffect(clone); return; } EffectsParameterList params = EffectsController::getEffectArgs(e); Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) { continue; } service.lock(); QString prefix; QString ser = effect->get("mlt_service"); if (ser == QLatin1String("region")) { prefix = QStringLiteral("filter0."); } for (int j = 0; j < params.count(); ++j) { effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData()); // qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId); // slotRefreshTracks(); */ } bool ClipController::hasEffects() const { return m_effectStack->rowCount() > 0; } void ClipController::setBinEffectsEnabled(bool enabled) { m_effectStack->setEffectStackEnabled(enabled); } void ClipController::saveZone(QPoint zone, const QDir &dir) { QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt")); if (dir.exists(path)) { // TODO ask for overwrite } Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); Mlt::Producer prod(m_masterProducer->get_producer()); Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y()); Mlt::Playlist list(pCore->getCurrentProfile()->profile()); list.insert_at(0, *prod2, 0); // list.set("title", desc.toUtf8().constData()); xmlConsumer.connect(list); xmlConsumer.run(); delete prod2; } std::shared_ptr ClipController::getEffectStack() const { return m_effectStack; } void ClipController::addEffect(const QString &effectId) { m_effectStack->appendEffect(effectId); } bool ClipController::copyEffect(std::shared_ptr stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId)); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } diff --git a/src/mltcontroller/clipcontroller.h b/src/mltcontroller/clipcontroller.h index 6f0ffe906..54c326da3 100644 --- a/src/mltcontroller/clipcontroller.h +++ b/src/mltcontroller/clipcontroller.h @@ -1,231 +1,231 @@ /* 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 CLIPCONTROLLER_H #define CLIPCONTROLLER_H #include "definitions.h" #include #include #include #include #include #include #include class QPixmap; class Bin; class BinController; class AudioStreamInfo; class EffectStackModel; class MarkerListModel; /** * @class ClipController * @brief Provides a convenience wrapper around the project Bin clip producers. * It also holds a QList of track producers for the 'master' producer in case we * need to update or replace them */ class ClipController { public: friend class Bin; /** * @brief Constructor. The constructor is protected because you should call the static Construct instead * @param bincontroller reference to the bincontroller * @param producer producer to create reference to */ explicit ClipController(const QString id, std::shared_ptr bincontroller, std::shared_ptr producer = nullptr); public: virtual ~ClipController(); /** @brief Returns true if the master producer is valid */ bool isValid(); /** @brief Stores the file's creation time */ QDateTime date; /** @brief Replaces the master producer and (TODO) the track producers with an updated producer, for example a proxy */ void updateProducer(const std::shared_ptr &producer); void getProducerXML(QDomDocument &document, bool includeMeta = false); /** @brief Returns a clone of our master producer. Delete after use! */ Mlt::Producer *masterProducer(); /** @brief Returns the clip name (usually file name) */ QString clipName() const; /** @brief Returns the clip's description or metadata comment */ QString description() const; /** @brief Returns the clip's MLT resource */ const QString clipUrl() const; /** @brief Returns the clip's type as defined in definitions.h */ ClipType clipType() const; /** @brief Returns the MLT's producer id */ const QString binId() const; /** @brief Returns the clip's duration */ GenTime getPlaytime() const; int getFramePlaytime() const; /** * @brief Sets a property. * @param name name of the property * @param value the new value */ void setProducerProperty(const QString &name, const QString &value); void setProducerProperty(const QString &name, int value); void setProducerProperty(const QString &name, double value); /** @brief Reset a property on the MLT producer (=delete the property). */ void resetProducerProperty(const QString &name); /** * @brief Returns the list of all properties starting with prefix. For subclips, the list is of this type: * { subclip name , subclip in/out } where the subclip in/ou value is a semi-colon separated in/out value, like "25;220" */ QMap getPropertiesFromPrefix(const QString &prefix, bool withPrefix = false); /** * @brief Returns the value of a property. * @param name name o the property */ QMap currentProperties(const QMap &props); QString getProducerProperty(const QString &key) const; int getProducerIntProperty(const QString &key) const; qint64 getProducerInt64Property(const QString &key) const; QColor getProducerColorProperty(const QString &key) const; double getProducerDoubleProperty(const QString &key) const; double originalFps() const; QString videoCodecProperty(const QString &property) const; const QString codec(bool audioCodec) const; const QString getClipHash() const; const QSize getFrameSize() const; /** @brief Returns the clip duration as a string like 00:00:02:01. */ const QString getStringDuration(); /** * @brief Returns a pixmap created from a frame of the producer. * @param position frame position * @param width width of the pixmap (only a guidance) * @param height height of the pixmap (only a guidance) */ QPixmap pixmap(int position = 0, int width = 0, int height = 0); /** @brief Returns the MLT producer's service. */ QString serviceName() const; /** @brief Returns the original master producer. */ std::shared_ptr originalProducer(); /** @brief Holds index of currently selected master clip effect. */ int selectedEffectIndex; /** @brief Get a clone of master producer for a specific track. Retrieve it if it already exists * in our list, otherwise we create it. * Deprecated, track logic should be handled in timeline/track.cpp */ Q_DECL_DEPRECATED Mlt::Producer *getTrackProducer(const QString &trackName, PlaylistState::ClipState clipState = PlaylistState::Original, double speed = 1.0); /** @brief Sets the master producer for this clip when we build the controller without master clip. */ void addMasterProducer(const std::shared_ptr &producer); /* @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; void setZone(const QPoint &zone); QPoint zone() const; bool hasLimitedDuration() const; Mlt::Properties &properties(); void initEffect(QDomElement &xml); void addEffect(QDomElement &xml); bool copyEffect(std::shared_ptr stackModel, int rowId); void removeEffect(int effectIndex, bool delayRefresh = false); EffectsList effectList(); /** @brief Enable/disable an effect. */ void changeEffectState(const QList &indexes, bool disable); void updateEffect(const QDomElement &e, int ix); /** @brief Returns true if the bin clip has effects */ bool hasEffects() const; /** @brief Returns info about clip audio */ - AudioStreamInfo *audioInfo() const; + const std::unique_ptr& audioInfo() const; /** @brief Returns true if audio thumbnails for this clip are cached */ bool m_audioThumbCreated; /** @brief When replacing a producer, it is important that we keep some properties, for exemple force_ stuff and url for proxies * this method returns a list of properties that we want to keep when replacing a producer . */ static const char *getPassPropertiesList(bool passLength = true); /** @brief Disable all Kdenlive effects on this clip */ void setBinEffectsEnabled(bool enabled); /** @brief Create a Kdenlive EffectList xml data from an MLT service */ static EffectsList xmlEffectList(Mlt::Profile *profile, Mlt::Service &service); /** @brief Returns the number of Kdenlive added effects for this bin clip */ int effectsCount(); /** @brief Move an effect in stack for this bin clip */ void moveEffect(int oldPos, int newPos); /** @brief Request an update of all track producers */ void reloadTrackProducers(); /** @brief Save an xml playlist of current clip with in/out points as zone.x()/y() */ void saveZone(QPoint zone, const QDir &dir); /* @brief This is the producer that serves as a placeholder while a clip is being loaded. It is created in Core at startup */ static std::shared_ptr mediaUnavailable; /** @brief Returns a ptr to the effetstack associated with this element */ std::shared_ptr getEffectStack() const; /** @brief Append an effect to this producer's effect list */ void addEffect(const QString &effectId); protected: virtual void emitProducerChanged(const QString& , const std::shared_ptr &) {}; virtual void connectEffectStack() {}; std::shared_ptr m_masterProducer; Mlt::Properties *m_properties; bool m_usesProxy; - AudioStreamInfo *m_audioInfo; + std::unique_ptr m_audioInfo; QString m_service; QString m_path; int m_audioIndex; int m_videoIndex; ClipType m_clipType; bool m_hasLimitedDuration; std::weak_ptr m_binController; QMutex m_effectMutex; void getInfoForProducer(); // void rebuildEffectList(ProfileInfo info); std::shared_ptr m_effectStack; std::shared_ptr m_markerModel; private: QMutex m_producerLock; QString m_controllerBinId; }; #endif diff --git a/src/mltcontroller/clippropertiescontroller.cpp b/src/mltcontroller/clippropertiescontroller.cpp index c3179141e..dcf561b47 100644 --- a/src/mltcontroller/clippropertiescontroller.cpp +++ b/src/mltcontroller/clippropertiescontroller.cpp @@ -1,1169 +1,1169 @@ /* Copyright (C) 2015 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 "clippropertiescontroller.h" #include "bin/model/markerlistmodel.hpp" #include "bincontroller.h" #include "clipcontroller.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "project/projectmanager.h" #include "profiles/profilerepository.hpp" #include "timecodedisplay.h" #include "utils/KoIconUtils.h" #include "widgets/choosecolorwidget.h" #include #ifdef KF5_USE_FILEMETADATA #include #include #include #include #endif #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AnalysisTree::AnalysisTree(QWidget *parent) : QTreeWidget(parent) { setRootIsDecorated(false); setColumnCount(2); setAlternatingRowColors(true); setHeaderHidden(true); setDragEnabled(true); } // virtual QMimeData *AnalysisTree::mimeData(const QList list) const { QString data; for (QTreeWidgetItem *item : list) { - if (item->flags() & Qt::ItemIsDragEnabled != 0) { + if ((item->flags() & Qt::ItemIsDragEnabled) != 0) { data.append(item->text(1)); } } auto *mime = new QMimeData; mime->setData(QStringLiteral("kdenlive/geometry"), data.toUtf8()); return mime; } #ifdef KF5_USE_FILEMETADATA class ExtractionResult : public KFileMetaData::ExtractionResult { public: ExtractionResult(const QString &filename, const QString &mimetype, QTreeWidget *tree) : KFileMetaData::ExtractionResult(filename, mimetype, KFileMetaData::ExtractionResult::ExtractMetaData) , m_tree(tree) { } void append(const QString & /*text*/) override {} void addType(KFileMetaData::Type::Type /*type*/) override {} void add(KFileMetaData::Property::Property property, const QVariant &value) override { bool decode = false; switch (property) { case KFileMetaData::Property::ImageMake: case KFileMetaData::Property::ImageModel: case KFileMetaData::Property::ImageDateTime: case KFileMetaData::Property::BitRate: case KFileMetaData::Property::TrackNumber: case KFileMetaData::Property::ReleaseYear: case KFileMetaData::Property::Composer: case KFileMetaData::Property::Genre: case KFileMetaData::Property::Artist: case KFileMetaData::Property::Album: case KFileMetaData::Property::Title: case KFileMetaData::Property::Comment: case KFileMetaData::Property::Copyright: case KFileMetaData::Property::PhotoFocalLength: case KFileMetaData::Property::PhotoExposureTime: case KFileMetaData::Property::PhotoFNumber: case KFileMetaData::Property::PhotoApertureValue: case KFileMetaData::Property::PhotoWhiteBalance: case KFileMetaData::Property::PhotoGpsLatitude: case KFileMetaData::Property::PhotoGpsLongitude: decode = true; break; default: break; } if (decode) { KFileMetaData::PropertyInfo info(property); if (info.valueType() == QVariant::DateTime) { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toDateTime().toString(Qt::DefaultLocaleShortDate)); } else if (info.valueType() == QVariant::Int) { int val = value.toInt(); if (property == KFileMetaData::Property::BitRate) { // Adjust unit for bitrate new QTreeWidgetItem( m_tree, QStringList() << info.displayName() << QString::number(val / 1000) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")); } else { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val)); } } else if (info.valueType() == QVariant::Double) { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(value.toDouble())); } else { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toString()); } } } private: QTreeWidget *m_tree; }; #endif ClipPropertiesController::ClipPropertiesController(ClipController *controller, QWidget *parent) : QWidget(parent) , m_controller(controller) , m_tc(Timecode(Timecode::HH_MM_SS_HH, pCore->getCurrentFps())) , m_id(controller->binId()) , m_type(controller->clipType()) , m_properties(controller->properties()) , m_textEdit(nullptr) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); auto *lay = new QVBoxLayout; lay->setContentsMargins(0, 0, 0, 0); m_clipLabel = new QLabel(controller->clipName()); lay->addWidget(m_clipLabel); m_tabWidget = new QTabWidget(this); lay->addWidget(m_tabWidget); setLayout(lay); m_tabWidget->setDocumentMode(true); m_tabWidget->setTabPosition(QTabWidget::East); m_forcePage = new QWidget(this); m_propertiesPage = new QWidget(this); m_markersPage = new QWidget(this); m_metaPage = new QWidget(this); m_analysisPage = new QWidget(this); // Clip properties auto *propsBox = new QVBoxLayout; m_propertiesTree = new QTreeWidget(this); m_propertiesTree->setRootIsDecorated(false); m_propertiesTree->setColumnCount(2); m_propertiesTree->setAlternatingRowColors(true); m_propertiesTree->sortByColumn(0, Qt::AscendingOrder); m_propertiesTree->setHeaderHidden(true); propsBox->addWidget(m_propertiesTree); fillProperties(); m_propertiesPage->setLayout(propsBox); // Clip markers auto *mBox = new QVBoxLayout; m_markerTree = new QTreeView; m_markerTree->setRootIsDecorated(false); m_markerTree->setAlternatingRowColors(true); m_markerTree->setHeaderHidden(true); m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection); m_markerTree->setModel(controller->getMarkerModel().get()); mBox->addWidget(m_markerTree); auto *bar = new QToolBar; bar->addAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker())); bar->addAction(KoIconUtils::themedIcon(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker())); bar->addAction(KoIconUtils::themedIcon(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker())); bar->addAction(KoIconUtils::themedIcon(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers())); bar->addAction(KoIconUtils::themedIcon(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers())); mBox->addWidget(bar); m_markersPage->setLayout(mBox); connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotSeekToMarker); // metadata auto *m2Box = new QVBoxLayout; auto *metaTree = new QTreeWidget; metaTree->setRootIsDecorated(true); metaTree->setColumnCount(2); metaTree->setAlternatingRowColors(true); metaTree->setHeaderHidden(true); m2Box->addWidget(metaTree); slotFillMeta(metaTree); m_metaPage->setLayout(m2Box); // Clip analysis auto *aBox = new QVBoxLayout; m_analysisTree = new AnalysisTree(this); aBox->addWidget(new QLabel(i18n("Analysis data"))); aBox->addWidget(m_analysisTree); auto *bar2 = new QToolBar; bar2->addAction(KoIconUtils::themedIcon(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis())); bar2->addAction(KoIconUtils::themedIcon(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis())); bar2->addAction(KoIconUtils::themedIcon(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis())); aBox->addWidget(bar2); slotFillAnalysisData(); m_analysisPage->setLayout(aBox); // Force properties auto *vbox = new QVBoxLayout; - if (m_type == Text || m_type == SlideShow || m_type == TextTemplate) { + if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) { QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this); connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip); vbox->addWidget(editButton); } - if (m_type == Color || m_type == Image || m_type == AV || m_type == Video || m_type == TextTemplate) { + if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) { // Edit duration widget m_originalProperties.insert(QStringLiteral("out"), m_properties.get("out")); int kdenlive_length = m_properties.get_int("kdenlive:duration"); if (kdenlive_length > 0) { m_originalProperties.insert(QStringLiteral("kdenlive:duration"), QString::number(kdenlive_length)); } m_originalProperties.insert(QStringLiteral("length"), m_properties.get("length")); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Duration"), this); box->setObjectName(QStringLiteral("force_duration")); hlay->addWidget(box); auto *timePos = new TimecodeDisplay(m_tc, this); timePos->setObjectName(QStringLiteral("force_duration_value")); timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties.get_int("length")); int original_length = m_properties.get_int("kdenlive:original_length"); if (original_length > 0) { box->setChecked(true); } else { timePos->setEnabled(false); } hlay->addWidget(timePos); vbox->addLayout(hlay); connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged); connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat); connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int))); } - if (m_type == TextTemplate) { + if (m_type == ClipType::TextTemplate) { // Edit text widget QString currentText = m_properties.get("templatetext"); m_originalProperties.insert(QStringLiteral("templatetext"), currentText); m_textEdit = new QTextEdit(this); m_textEdit->setAcceptRichText(false); m_textEdit->setPlainText(currentText); m_textEdit->setPlaceholderText(i18n("Enter template text here")); vbox->addWidget(m_textEdit); QPushButton *button = new QPushButton(i18n("Apply"), this); vbox->addWidget(button); connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged); - } else if (m_type == Color) { + } else if (m_type == ClipType::Color) { // Edit color widget m_originalProperties.insert(QStringLiteral("resource"), m_properties.get("resource")); mlt_color color = m_properties.get_color("resource"); ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this); vbox->addWidget(choosecolor); // connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int))); connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified); connect(this, SIGNAL(modified(QColor)), choosecolor, SLOT(slotColorModified(QColor))); - } else if (m_type == Image) { + } else if (m_type == ClipType::Image) { int transparency = m_properties.get_int("kdenlive:transparency"); m_originalProperties.insert(QStringLiteral("kdenlive:transparency"), QString::number(transparency)); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Transparent"), this); box->setObjectName(QStringLiteral("kdenlive:transparency")); box->setChecked(transparency == 1); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); hlay->addWidget(box); vbox->addLayout(hlay); } - if (m_type == AV || m_type == Video || m_type == Image) { + if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) { // Aspect ratio int force_ar_num = m_properties.get_int("force_aspect_num"); int force_ar_den = m_properties.get_int("force_aspect_den"); m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den)); m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num)); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Aspect Ratio"), this); box->setObjectName(QStringLiteral("force_ar")); vbox->addWidget(box); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); auto *spin1 = new QSpinBox(this); spin1->setMaximum(8000); spin1->setObjectName(QStringLiteral("force_aspect_num_value")); hlay->addWidget(spin1); hlay->addWidget(new QLabel(QStringLiteral(":"))); auto *spin2 = new QSpinBox(this); spin2->setMinimum(1); spin2->setMaximum(8000); spin2->setObjectName(QStringLiteral("force_aspect_den_value")); hlay->addWidget(spin2); if (force_ar_num == 0) { // use current ratio int num = m_properties.get_int("meta.media.sample_aspect_num"); int den = m_properties.get_int("meta.media.sample_aspect_den"); if (den == 0) { num = 1; den = 1; } spin1->setEnabled(false); spin2->setEnabled(false); spin1->setValue(num); spin2->setValue(den); } else { box->setChecked(true); spin1->setEnabled(true); spin2->setEnabled(true); spin1->setValue(force_ar_num); spin2->setValue(force_ar_den); } connect(spin2, SIGNAL(valueChanged(int)), this, SLOT(slotAspectValueChanged(int))); connect(spin1, SIGNAL(valueChanged(int)), this, SLOT(slotAspectValueChanged(int))); connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled); connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled); vbox->addLayout(hlay); } - if (m_type == AV || m_type == Video) { + if (m_type == ClipType::AV || m_type == ClipType::Video) { QLocale locale; // Fps QString force_fps = m_properties.get("force_fps"); m_originalProperties.insert(QStringLiteral("force_fps"), force_fps.isEmpty() ? QStringLiteral("-") : force_fps); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Frame rate"), this); box->setObjectName(QStringLiteral("force_fps")); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); auto *spin = new QDoubleSpinBox(this); spin->setMaximum(1000); connect(spin, SIGNAL(valueChanged(double)), this, SLOT(slotValueChanged(double))); spin->setObjectName(QStringLiteral("force_fps_value")); if (force_fps.isEmpty()) { spin->setValue(controller->originalFps()); } else { spin->setValue(locale.toDouble(force_fps)); } connect(box, &QAbstractButton::toggled, spin, &QWidget::setEnabled); box->setChecked(!force_fps.isEmpty()); spin->setEnabled(!force_fps.isEmpty()); hlay->addWidget(box); hlay->addWidget(spin); vbox->addLayout(hlay); // Scanning QString force_prog = m_properties.get("force_progressive"); m_originalProperties.insert(QStringLiteral("force_progressive"), force_prog.isEmpty() ? QStringLiteral("-") : force_prog); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Scanning"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("force_progressive")); auto *combo = new QComboBox(this); combo->addItem(i18n("Interlaced"), 0); combo->addItem(i18n("Progressive"), 1); connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotComboValueChanged())); combo->setObjectName(QStringLiteral("force_progressive_value")); if (!force_prog.isEmpty()) { combo->setCurrentIndex(force_prog.toInt()); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); box->setChecked(!force_prog.isEmpty()); combo->setEnabled(!force_prog.isEmpty()); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Field order QString force_tff = m_properties.get("force_tff"); m_originalProperties.insert(QStringLiteral("force_tff"), force_tff.isEmpty() ? QStringLiteral("-") : force_tff); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Field order"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("force_tff")); combo = new QComboBox(this); combo->addItem(i18n("Bottom first"), 0); combo->addItem(i18n("Top first"), 1); connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotComboValueChanged())); combo->setObjectName(QStringLiteral("force_tff_value")); if (!force_tff.isEmpty()) { combo->setCurrentIndex(force_tff.toInt()); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); box->setChecked(!force_tff.isEmpty()); combo->setEnabled(!force_tff.isEmpty()); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Autorotate QString autorotate = m_properties.get("autorotate"); m_originalProperties.insert(QStringLiteral("autorotate"), autorotate); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Disable autorotate"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("autorotate")); box->setChecked(autorotate == QLatin1String("0")); hlay->addWidget(box); vbox->addLayout(hlay); // Decoding threads QString threads = m_properties.get("threads"); m_originalProperties.insert(QStringLiteral("threads"), threads); hlay = new QHBoxLayout; hlay->addWidget(new QLabel(i18n("Threads"))); auto *spinI = new QSpinBox(this); spinI->setMaximum(4); spinI->setObjectName(QStringLiteral("threads_value")); if (!threads.isEmpty()) { spinI->setValue(threads.toInt()); } else { spinI->setValue(1); } connect(spinI, SIGNAL(valueChanged(int)), this, SLOT(slotValueChanged(int))); hlay->addWidget(spinI); vbox->addLayout(hlay); // Video index QString vix = m_properties.get("video_index"); m_originalProperties.insert(QStringLiteral("video_index"), vix); hlay = new QHBoxLayout; hlay->addWidget(new QLabel(i18n("Video index"))); spinI = new QSpinBox(this); spinI->setMaximum(m_clipProperties.value(QStringLiteral("video_max")).toInt()); spinI->setObjectName(QStringLiteral("video_index_value")); if (vix.isEmpty()) { spinI->setValue(0); } else { spinI->setValue(vix.toInt()); } connect(spinI, SIGNAL(valueChanged(int)), this, SLOT(slotValueChanged(int))); hlay->addWidget(spinI); vbox->addLayout(hlay); // Audio index QString aix = m_properties.get("audio_index"); m_originalProperties.insert(QStringLiteral("audio_index"), aix); hlay = new QHBoxLayout; hlay->addWidget(new QLabel(i18n("Audio index"))); spinI = new QSpinBox(this); spinI->setMaximum(m_clipProperties.value(QStringLiteral("audio_max")).toInt()); spinI->setObjectName(QStringLiteral("audio_index_value")); if (aix.isEmpty()) { spinI->setValue(0); } else { spinI->setValue(aix.toInt()); } connect(spinI, SIGNAL(valueChanged(int)), this, SLOT(slotValueChanged(int))); hlay->addWidget(spinI); vbox->addLayout(hlay); // Colorspace hlay = new QHBoxLayout; box = new QCheckBox(i18n("Colorspace"), this); box->setObjectName(QStringLiteral("force_colorspace")); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); combo = new QComboBox(this); combo->setObjectName(QStringLiteral("force_colorspace_value")); combo->addItem(ProfileRepository::getColorspaceDescription(601), 601); combo->addItem(ProfileRepository::getColorspaceDescription(709), 709); combo->addItem(ProfileRepository::getColorspaceDescription(240), 240); int force_colorspace = m_properties.get_int("force_colorspace"); m_originalProperties.insert(QStringLiteral("force_colorspace"), force_colorspace == 0 ? QStringLiteral("-") : QString::number(force_colorspace)); int colorspace = controller->videoCodecProperty(QStringLiteral("colorspace")).toInt(); if (force_colorspace > 0) { box->setChecked(true); combo->setEnabled(true); combo->setCurrentIndex(combo->findData(force_colorspace)); } else if (colorspace > 0) { combo->setEnabled(false); combo->setCurrentIndex(combo->findData(colorspace)); } else { combo->setEnabled(false); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotComboValueChanged())); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Full luma QString force_luma = m_properties.get("set.force_full_luma"); m_originalProperties.insert(QStringLiteral("set.force_full_luma"), force_luma); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Full luma range"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("set.force_full_luma")); box->setChecked(!force_luma.isEmpty()); hlay->addWidget(box); vbox->addLayout(hlay); } m_forcePage->setLayout(vbox); vbox->addStretch(10); m_tabWidget->addTab(m_propertiesPage, QString()); m_tabWidget->addTab(m_forcePage, QString()); m_tabWidget->addTab(m_markersPage, QString()); m_tabWidget->addTab(m_metaPage, QString()); m_tabWidget->addTab(m_analysisPage, QString()); m_tabWidget->setTabIcon(0, KoIconUtils::themedIcon(QStringLiteral("edit-find"))); m_tabWidget->setTabToolTip(0, i18n("Properties")); m_tabWidget->setTabIcon(1, KoIconUtils::themedIcon(QStringLiteral("document-edit"))); m_tabWidget->setTabToolTip(1, i18n("Force properties")); m_tabWidget->setTabIcon(2, KoIconUtils::themedIcon(QStringLiteral("bookmark-new"))); m_tabWidget->setTabToolTip(2, i18n("Markers")); m_tabWidget->setTabIcon(3, KoIconUtils::themedIcon(QStringLiteral("view-grid"))); m_tabWidget->setTabToolTip(3, i18n("Metadata")); m_tabWidget->setTabIcon(4, KoIconUtils::themedIcon(QStringLiteral("visibility"))); m_tabWidget->setTabToolTip(4, i18n("Analysis")); m_tabWidget->setCurrentIndex(KdenliveSettings::properties_panel_page()); - if (m_type == Color) { + if (m_type == ClipType::Color) { m_tabWidget->setTabEnabled(0, false); } connect(m_tabWidget, &QTabWidget::currentChanged, this, &ClipPropertiesController::updateTab); } ClipPropertiesController::~ClipPropertiesController() { } void ClipPropertiesController::updateTab(int ix) { KdenliveSettings::setProperties_panel_page(ix); } void ClipPropertiesController::slotRefreshTimeCode() { emit updateTimeCodeFormat(); } void ClipPropertiesController::slotReloadProperties() { mlt_color color; m_clipLabel->setText(m_properties.get("kdenlive:clipname")); switch (m_type) { - case Color: + case ClipType::Color: m_originalProperties.insert(QStringLiteral("resource"), m_properties.get("resource")); m_originalProperties.insert(QStringLiteral("out"), m_properties.get("out")); m_originalProperties.insert(QStringLiteral("length"), m_properties.get("length")); emit modified(m_properties.get_int("length")); color = m_properties.get_color("resource"); emit modified(QColor::fromRgb(color.r, color.g, color.b)); break; - case TextTemplate: + case ClipType::TextTemplate: m_textEdit->setPlainText(m_properties.get("templatetext")); break; default: break; } } void ClipPropertiesController::slotColorModified(const QColor &newcolor) { QMap properties; properties.insert(QStringLiteral("resource"), newcolor.name(QColor::HexArgb)); QMap oldProperties; oldProperties.insert(QStringLiteral("resource"), m_properties.get("resource")); emit updateClipProperties(m_id, oldProperties, properties); } void ClipPropertiesController::slotDurationChanged(int duration) { QMap properties; int original_length = m_properties.get_int("kdenlive:original_length"); // kdenlive_length is the default duration for image / title clips int kdenlive_length = m_properties.get_int("kdenlive:duration"); int current_length = m_properties.get_int("length"); if (original_length == 0) { m_properties.set("kdenlive:original_length", kdenlive_length > 0 ? kdenlive_length : current_length); } if (kdenlive_length > 0) { // special case, image/title clips store default duration in kdenlive:duration property properties.insert(QStringLiteral("kdenlive:duration"), QString::number(duration)); if (duration > current_length) { properties.insert(QStringLiteral("length"), QString::number(duration)); properties.insert(QStringLiteral("out"), QString::number(duration - 1)); } } else { properties.insert(QStringLiteral("length"), QString::number(duration)); properties.insert(QStringLiteral("out"), QString::number(duration - 1)); } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotEnableForce(int state) { QCheckBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName(); QMap properties; QLocale locale; if (state == Qt::Unchecked) { // The force property was disable, remove it / reset default if necessary if (param == QLatin1String("force_duration")) { // special case, reset original duration TimecodeDisplay *timePos = findChild(param + QStringLiteral("_value")); timePos->setValue(m_properties.get_int("kdenlive:original_length")); slotDurationChanged(m_properties.get_int("kdenlive:original_length")); m_properties.set("kdenlive:original_length", (char *)nullptr); return; } if (param == QLatin1String("kdenlive:transparency")) { properties.insert(param, QString()); } else if (param == QLatin1String("force_ar")) { properties.insert(QStringLiteral("force_aspect_den"), QString()); properties.insert(QStringLiteral("force_aspect_num"), QString()); properties.insert(QStringLiteral("force_aspect_ratio"), QString()); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QString()); } else { properties.insert(param, QString()); } } else { // A force property was set if (param == QLatin1String("force_duration")) { int original_length = m_properties.get_int("kdenlive:original_length"); if (original_length == 0) { int kdenlive_duration = m_properties.get_int("kdenlive:duration"); m_properties.set("kdenlive:original_length", kdenlive_duration > 0 ? kdenlive_duration : m_properties.get_int("length")); } } else if (param == QLatin1String("force_fps")) { QDoubleSpinBox *spin = findChild(param + QStringLiteral("_value")); if (!spin) { return; } properties.insert(param, locale.toString(spin->value())); } else if (param == QLatin1String("threads")) { QSpinBox *spin = findChild(param + QStringLiteral("_value")); if (!spin) { return; } properties.insert(param, QString::number(spin->value())); } else if (param == QLatin1String("force_colorspace") || param == QLatin1String("force_progressive") || param == QLatin1String("force_tff")) { QComboBox *combo = findChild(param + QStringLiteral("_value")); if (!combo) { return; } properties.insert(param, QString::number(combo->currentData().toInt())); } else if (param == QLatin1String("kdenlive:transparency") || param == QLatin1String("set.force_full_luma")) { properties.insert(param, QStringLiteral("1")); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QStringLiteral("0")); } else if (param == QLatin1String("force_ar")) { QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value())); } } if (properties.isEmpty()) { return; } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotValueChanged(double value) { QDoubleSpinBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; QLocale locale; properties.insert(param, locale.toString(value)); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotValueChanged(int value) { QSpinBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(value)); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotAspectValueChanged(int) { QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } QMap properties; properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); QLocale locale; properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotComboValueChanged() { QComboBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(box->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::fillProperties() { m_clipProperties.clear(); QList propertyMap; m_propertiesTree->setSortingEnabled(false); #ifdef KF5_USE_FILEMETADATA // Read File Metadata through KDE's metadata system KFileMetaData::ExtractorCollection metaDataCollection; QMimeDatabase mimeDatabase; QMimeType mimeType; mimeType = mimeDatabase.mimeTypeForFile(m_controller->clipUrl()); for (KFileMetaData::Extractor *plugin : metaDataCollection.fetchExtractors(mimeType.name())) { ExtractionResult extractionResult(m_controller->clipUrl(), mimeType.name(), m_propertiesTree); plugin->extract(&extractionResult); } #endif // Get MLT's metadata - if (m_type == Image) { + if (m_type == ClipType::Image) { int width = m_controller->getProducerIntProperty(QStringLiteral("meta.media.width")); int height = m_controller->getProducerIntProperty(QStringLiteral("meta.media.height")); propertyMap.append(QStringList() << i18n("Image size") << QString::number(width) + QLatin1Char('x') + QString::number(height)); } - if (m_type == AV || m_type == Video || m_type == Audio) { + if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Audio) { int vindex = m_controller->getProducerIntProperty(QStringLiteral("video_index")); int video_max = 0; int default_audio = m_controller->getProducerIntProperty(QStringLiteral("audio_index")); int audio_max = 0; // Find maximum stream index values for (int ix = 0; ix < m_controller->getProducerIntProperty(QStringLiteral("meta.media.nb_streams")); ++ix) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.type", ix); QString type = m_controller->getProducerProperty(property); if (type == QLatin1String("video")) { video_max = ix; } else if (type == QLatin1String("audio")) { audio_max = ix; } } m_clipProperties.insert(QStringLiteral("default_video"), QString::number(vindex)); m_clipProperties.insert(QStringLiteral("video_max"), QString::number(video_max)); m_clipProperties.insert(QStringLiteral("default_audio"), QString::number(default_audio)); m_clipProperties.insert(QStringLiteral("audio_max"), QString::number(audio_max)); if (vindex > -1) { // We have a video stream char property[200]; snprintf(property, sizeof(property), "meta.media.%d.codec.long_name", vindex); QString codec = m_controller->getProducerProperty(property); if (!codec.isEmpty()) { propertyMap.append(QStringList() << i18n("Video codec") << codec); } int width = m_controller->getProducerIntProperty(QStringLiteral("meta.media.width")); int height = m_controller->getProducerIntProperty(QStringLiteral("meta.media.height")); propertyMap.append(QStringList() << i18n("Frame size") << QString::number(width) + QLatin1Char('x') + QString::number(height)); snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); QString fpsValue = m_controller->getProducerProperty(property); if (!fpsValue.isEmpty()) { propertyMap.append(QStringList() << i18n("Frame rate") << fpsValue); } else { int rate_den = m_controller->getProducerIntProperty(QStringLiteral("meta.media.frame_rate_den")); if (rate_den > 0) { double fps = (double)m_controller->getProducerIntProperty(QStringLiteral("meta.media.frame_rate_num")) / rate_den; propertyMap.append(QStringList() << i18n("Frame rate") << QString::number(fps, 'f', 2)); } } snprintf(property, sizeof(property), "meta.media.%d.codec.bit_rate", vindex); int bitrate = m_controller->getProducerIntProperty(property) / 1000; if (bitrate > 0) { propertyMap.append(QStringList() << i18n("Video bitrate") << QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")); } int scan = m_controller->getProducerIntProperty(QStringLiteral("meta.media.progressive")); propertyMap.append(QStringList() << i18n("Scanning") << (scan == 1 ? i18n("Progressive") : i18n("Interlaced"))); snprintf(property, sizeof(property), "meta.media.%d.codec.sample_aspect_ratio", vindex); double par = m_controller->getProducerDoubleProperty(property); if (par == 0) { // Read media aspect ratio par = m_controller->getProducerDoubleProperty(QStringLiteral("aspect_ratio")); } propertyMap.append(QStringList() << i18n("Pixel aspect ratio") << QString::number(par, 'f', 3)); propertyMap.append(QStringList() << i18n("Pixel format") << m_controller->videoCodecProperty(QStringLiteral("pix_fmt"))); int colorspace = m_controller->videoCodecProperty(QStringLiteral("colorspace")).toInt(); propertyMap.append(QStringList() << i18n("Colorspace") << ProfileRepository::getColorspaceDescription(colorspace)); } if (default_audio > -1) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.codec.long_name", default_audio); QString codec = m_controller->getProducerProperty(property); if (!codec.isEmpty()) { propertyMap.append(QStringList() << i18n("Audio codec") << codec); } snprintf(property, sizeof(property), "meta.media.%d.codec.channels", default_audio); int channels = m_controller->getProducerIntProperty(property); propertyMap.append(QStringList() << i18n("Audio channels") << QString::number(channels)); snprintf(property, sizeof(property), "meta.media.%d.codec.sample_rate", default_audio); int srate = m_controller->getProducerIntProperty(property); propertyMap.append(QStringList() << i18n("Audio frequency") << QString::number(srate) + QLatin1Char(' ') + i18nc("Herz", "Hz")); snprintf(property, sizeof(property), "meta.media.%d.codec.bit_rate", default_audio); int bitrate = m_controller->getProducerIntProperty(property) / 1000; if (bitrate > 0) { propertyMap.append(QStringList() << i18n("Audio bitrate") << QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")); } } } qint64 filesize = m_controller->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); if (filesize > 0) { QLocale locale(QLocale::system()); // use the user's locale for getting proper separators! propertyMap.append(QStringList() << i18n("File size") << KIO::convertSize(filesize) + QStringLiteral(" (") + locale.toString(filesize) + QLatin1Char(')')); } for (int i = 0; i < propertyMap.count(); i++) { new QTreeWidgetItem(m_propertiesTree, propertyMap.at(i)); } m_propertiesTree->setSortingEnabled(true); m_propertiesTree->resizeColumnToContents(0); } void ClipPropertiesController::slotSeekToMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); emit seekToFrame(pos.frames(pCore->getCurrentFps())); } void ClipPropertiesController::slotEditMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->editMarkerGui(pos, this, false, m_controller); } void ClipPropertiesController::slotDeleteMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->removeMarker(pos); } void ClipPropertiesController::slotAddMarker() { auto markerModel = m_controller->getMarkerModel(); GenTime pos(m_controller->originalProducer()->position(), m_tc.fps()); markerModel->editMarkerGui(pos, this, true, m_controller); } void ClipPropertiesController::slotSaveMarkers() { QScopedPointer fd(new QFileDialog(this, i18n("Save Clip Markers"), pCore->projectManager()->current()->projectDataFolder())); fd->setMimeTypeFilters(QStringList() << QStringLiteral("text/plain")); fd->setFileMode(QFileDialog::AnyFile); fd->setAcceptMode(QFileDialog::AcceptSave); if (fd->exec() != QDialog::Accepted) { return; } QStringList selection = fd->selectedFiles(); QString url; if (!selection.isEmpty()) { url = selection.first(); } if (url.isEmpty()) { return; } QFile file(url); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName())); return; } file.write(m_controller->getMarkerModel()->toJson().toUtf8()); file.close(); } void ClipPropertiesController::slotLoadMarkers() { QScopedPointer fd(new QFileDialog(this, i18n("Load Clip Markers"), pCore->projectManager()->current()->projectDataFolder())); fd->setMimeTypeFilters(QStringList() << QStringLiteral("text/plain")); fd->setFileMode(QFileDialog::ExistingFile); if (fd->exec() != QDialog::Accepted) { return; } QStringList selection = fd->selectedFiles(); QString url; if (!selection.isEmpty()) { url = selection.first(); } if (url.isEmpty()) { return; } QFile file(url); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName())); return; } QString fileContent = QString::fromUtf8(file.readAll()); file.close(); bool res = m_controller->getMarkerModel()->importFromJson(fileContent, false); if (!res) { KMessageBox::error(this, i18n("An error occurred while parsing the marker file")); } } void ClipPropertiesController::slotFillMeta(QTreeWidget *tree) { tree->clear(); - if (m_type != AV && m_type != Video && m_type != Image) { + if (m_type != ClipType::AV && m_type != ClipType::Video && m_type != ClipType::Image) { // Currently, we only use exiftool on video files return; } int exifUsed = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:exiftool")); if (exifUsed == 1) { Mlt::Properties subProperties; subProperties.pass_values(m_properties, "kdenlive:meta.exiftool."); if (subProperties.count() > 0) { QTreeWidgetItem *exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); for (int i = 0; i < subProperties.count(); i++) { new QTreeWidgetItem(exif, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } } } else if (KdenliveSettings::use_exiftool()) { QString url = m_controller->clipUrl(); // Check for Canon THM file url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".THM"); if (QFile::exists(url)) { // Read the exif metadata embedded in the THM file QProcess p; QStringList args; args << QStringLiteral("-g") << QStringLiteral("-args") << url; p.start(QStringLiteral("exiftool"), args); p.waitForFinished(); QString res = p.readAllStandardOutput(); m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1); QTreeWidgetItem *exif = nullptr; QStringList list = res.split(QLatin1Char('\n')); for (const QString &tagline : list) { if (tagline.startsWith(QLatin1String("-File")) || tagline.startsWith(QLatin1String("-ExifTool"))) { continue; } QString tag = tagline.section(QLatin1Char(':'), 1).simplified(); if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) { continue; } if (!tag.section(QLatin1Char('='), 0, 0).isEmpty() && !tag.section(QLatin1Char('='), 1).simplified().isEmpty()) { if (!exif) { exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); } m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0), tag.section(QLatin1Char('='), 1).simplified()); new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified()); } } } else { - if (m_type == Image || m_controller->codec(false) == QLatin1String("h264")) { + if (m_type == ClipType::Image || m_controller->codec(false) == QLatin1String("h264")) { QProcess p; QStringList args; args << QStringLiteral("-g") << QStringLiteral("-args") << m_controller->clipUrl(); p.start(QStringLiteral("exiftool"), args); p.waitForFinished(); QString res = p.readAllStandardOutput(); - if (m_type != Image) { + if (m_type != ClipType::Image) { m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1); } QTreeWidgetItem *exif = nullptr; QStringList list = res.split(QLatin1Char('\n')); for (const QString &tagline : list) { - if (m_type != Image && !tagline.startsWith(QLatin1String("-H264"))) { + if (m_type != ClipType::Image && !tagline.startsWith(QLatin1String("-H264"))) { continue; } QString tag = tagline.section(QLatin1Char(':'), 1); if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) { continue; } if (!exif) { exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); } - if (m_type != Image) { + if (m_type != ClipType::Image) { // Do not store image exif metadata in project file, would be too much noise m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0), tag.section(QLatin1Char('='), 1).simplified()); } new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified()); } } } } int magic = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:magiclantern")); if (magic == 1) { Mlt::Properties subProperties; subProperties.pass_values(m_properties, "kdenlive:meta.magiclantern."); QTreeWidgetItem *magicL = nullptr; for (int i = 0; i < subProperties.count(); i++) { if (!magicL) { magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString()); QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png"))); magicL->setIcon(0, icon); magicL->setExpanded(true); } new QTreeWidgetItem(magicL, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } - } else if (m_type != Image && KdenliveSettings::use_magicLantern()) { + } else if (m_type != ClipType::Image && KdenliveSettings::use_magicLantern()) { QString url = m_controller->clipUrl(); url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".LOG"); if (QFile::exists(url)) { QFile file(url); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { m_controller->setProducerProperty(QStringLiteral("kdenlive:magiclantern"), 1); QTreeWidgetItem *magicL = nullptr; while (!file.atEnd()) { QString line = file.readLine().simplified(); if (line.startsWith('#') || line.isEmpty() || !line.contains(QLatin1Char(':'))) { continue; } if (line.startsWith(QLatin1String("CSV data"))) { break; } m_controller->setProducerProperty("kdenlive:meta.magiclantern." + line.section(QLatin1Char(':'), 0, 0).simplified(), line.section(QLatin1Char(':'), 1).simplified()); if (!magicL) { magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString()); QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png"))); magicL->setIcon(0, icon); magicL->setExpanded(true); } new QTreeWidgetItem(magicL, QStringList() << line.section(QLatin1Char(':'), 0, 0).simplified() << line.section(QLatin1Char(':'), 1).simplified()); } } } // if (!meta.isEmpty()) // clip->setMetadata(meta, "Magic Lantern"); // clip->setProperty("magiclantern", "1"); } tree->resizeColumnToContents(0); } void ClipPropertiesController::slotFillAnalysisData() { m_analysisTree->clear(); Mlt::Properties subProperties; subProperties.pass_values(m_properties, "kdenlive:clipanalysis."); if (subProperties.count() > 0) { for (int i = 0; i < subProperties.count(); i++) { new QTreeWidgetItem(m_analysisTree, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } } m_analysisTree->resizeColumnToContents(0); } void ClipPropertiesController::slotDeleteAnalysis() { QTreeWidgetItem *current = m_analysisTree->currentItem(); if (!current) { return; } emit editAnalysis(m_id, "kdenlive:clipanalysis." + current->text(0), QString()); } void ClipPropertiesController::slotSaveAnalysis() { const QString url = QFileDialog::getSaveFileName(this, i18n("Save Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)")); if (url.isEmpty()) { return; } KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig); KConfigGroup analysisConfig(config, "Analysis"); QTreeWidgetItem *current = m_analysisTree->currentItem(); analysisConfig.writeEntry(current->text(0), current->text(1)); } void ClipPropertiesController::slotLoadAnalysis() { const QString url = QFileDialog::getOpenFileName(this, i18n("Open Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)")); if (url.isEmpty()) { return; } KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig); KConfigGroup transConfig(config, "Analysis"); // read the entries QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); emit editAnalysis(m_id, "kdenlive:clipanalysis." + i.key(), i.value()); } } void ClipPropertiesController::slotTextChanged() { QMap properties; properties.insert(QStringLiteral("templatetext"), m_textEdit->toPlainText()); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } diff --git a/src/mltcontroller/producerqueue.cpp b/src/mltcontroller/producerqueue.cpp index 1b1930d8d..fe4f074ca 100644 --- a/src/mltcontroller/producerqueue.cpp +++ b/src/mltcontroller/producerqueue.cpp @@ -1,983 +1,983 @@ /* Copyright (C) 2016 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 "producerqueue.h" #include "bin/projectclip.h" #include "bincontroller.h" #include "bin/projectitemmodel.h" #include "clipcontroller.h" #include "doc/kdenlivedoc.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/slideshowclip.h" #include "mltcontroller/clip.h" #include ProducerQueue::ProducerQueue(std::shared_ptr controller) : QObject(controller.get()) , m_binController(controller) { connect(this, SIGNAL(multiStreamFound(QString, QList, QList, stringMap)), this, SLOT(slotMultiStreamProducerFound(QString, QList, QList, stringMap))); } ProducerQueue::~ProducerQueue() { abortOperations(); } void ProducerQueue::getFileProperties(const QDomElement &xml, const QString &clipId, int imageHeight, bool replaceProducer) { // Make sure we don't request the info for same clip twice m_infoMutex.lock(); if (m_processingClipId.contains(clipId)) { m_infoMutex.unlock(); return; } for (int i = 0; i < m_requestList.count(); ++i) { if (m_requestList.at(i).clipId == clipId) { // Clip is already queued m_infoMutex.unlock(); return; } } requestClipInfo info; info.xml = xml; info.clipId = clipId; info.imageHeight = imageHeight; info.replaceProducer = replaceProducer; m_requestList.append(info); m_infoMutex.unlock(); if (!m_infoThread.isRunning()) { m_infoThread = QtConcurrent::run(this, &ProducerQueue::processFileProperties); } } void ProducerQueue::forceProcessing(const QString &id) { // Make sure we load the clip producer now so that we can use it in timeline QList requestListCopy; if (m_processingClipId.contains(id)) { m_infoMutex.lock(); requestListCopy = m_requestList; m_requestList.clear(); m_infoMutex.unlock(); m_infoThread.waitForFinished(); emit infoProcessingFinished(); } else { m_infoMutex.lock(); for (int i = 0; i < m_requestList.count(); ++i) { requestClipInfo info = m_requestList.at(i); if (info.clipId == id) { m_requestList.removeAt(i); requestListCopy = m_requestList; m_requestList.clear(); m_requestList.append(info); break; } } m_infoMutex.unlock(); if (!m_infoThread.isRunning()) { m_infoThread = QtConcurrent::run(this, &ProducerQueue::processFileProperties); } m_infoThread.waitForFinished(); emit infoProcessingFinished(); } m_infoMutex.lock(); m_requestList.append(requestListCopy); m_infoMutex.unlock(); if (!m_infoThread.isRunning()) { m_infoThread = QtConcurrent::run(this, &ProducerQueue::processFileProperties); } } void ProducerQueue::slotProcessingDone(const QString &id) { QMutexLocker lock(&m_infoMutex); m_processingClipId.removeAll(id); } bool ProducerQueue::isProcessing(const QString &id) { if (m_processingClipId.contains(id)) { return true; } QMutexLocker lock(&m_infoMutex); for (int i = 0; i < m_requestList.count(); ++i) { if (m_requestList.at(i).clipId == id) { return true; } } return false; } void ProducerQueue::processFileProperties() { requestClipInfo info; QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); bool forceThumbScale = qAbs(pCore->getCurrentSar() - 1) > 1e-1; while (!m_requestList.isEmpty()) { m_infoMutex.lock(); info = m_requestList.takeFirst(); if (info.xml.hasAttribute(QStringLiteral("thumbnailOnly")) || info.xml.hasAttribute(QStringLiteral("refreshOnly"))) { m_infoMutex.unlock(); // Special case, we just want the thumbnail for existing producer std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(info.clipId); Mlt::Producer *prod = new Mlt::Producer(*binClip->originalProducer()); if ((prod == nullptr) || !prod->is_valid()) { continue; } // Check if we are using GPU accel, then we need to use alternate producer if (KdenliveSettings::gpu_accel()) { QString service = prod->get("mlt_service"); QString res = prod->get("resource"); delete prod; prod = new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), res.toUtf8().constData()); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); prod->attach(scaler); prod->attach(converter); } int frameNumber = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:thumbnailFrame"), QStringLiteral("-1")).toInt(); if (frameNumber > 0) { prod->seek(frameNumber); } Mlt::Frame *frame = prod->get_frame(); if ((frame != nullptr) && frame->is_valid()) { int fullWidth = info.imageHeight * pCore->getCurrentDar() + 0.5; QImage img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); emit replyGetImage(info.clipId, img); } delete frame; delete prod; if (info.xml.hasAttribute(QStringLiteral("refreshOnly"))) { // inform timeline about change emit refreshTimelineProducer(info.clipId); } continue; } m_processingClipId.append(info.clipId); m_infoMutex.unlock(); // TODO: read all xml meta.kdenlive properties into a QMap or an MLT::Properties and pass them to the newly created producer QString path; bool proxyProducer; QString proxy = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:proxy")); if (!proxy.isEmpty()) { if (proxy == QLatin1String("-")) { path = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:originalurl")); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } proxyProducer = false; } else { path = proxy; // Check for missing proxies if (QFileInfo(path).size() <= 0) { // proxy is missing, re-create it emit requestProxy(info.clipId); proxyProducer = false; // path = info.xml.attribute("resource"); path = ProjectClip::getXmlProperty(info.xml, QStringLiteral("resource")); } else { proxyProducer = true; } } } else { path = ProjectClip::getXmlProperty(info.xml, QStringLiteral("resource")); // path = info.xml.attribute("resource"); proxyProducer = false; } // qCDebug(KDENLIVE_LOG)<<" / / /CHECKING PRODUCER PATH: "< producer; ClipType type = (ClipType)info.xml.attribute(QStringLiteral("type")).toInt(); - if (type == Unknown) { + if (type == ClipType::Unknown) { type = getTypeForService(ProjectClip::getXmlProperty(info.xml, QStringLiteral("mlt_service")), path); } - if (type == Color) { + if (type == ClipType::Color) { path.prepend(QStringLiteral("color:")); producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); - } else if (type == Text || type == TextTemplate) { + } else if (type == ClipType::Text || type == ClipType::TextTemplate) { path.prepend(QStringLiteral("kdenlivetitle:")); producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); - } else if (type == QText) { + } else if (type == ClipType::QText) { path.prepend(QStringLiteral("qtext:")); producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); - } else if (type == Playlist && !proxyProducer) { + } else if (type == ClipType::Playlist && !proxyProducer) { // TODO: "xml" seems to corrupt project fps if different, and "consumer" crashed on audio transition std::unique_ptr xmlProfile(new Mlt::Profile()); xmlProfile->set_explicit(0); // path.prepend("consumer:"); producer = std::make_shared(*xmlProfile, "xml", path.toUtf8().constData()); if (!producer->is_valid()) { m_processingClipId.removeAll(info.clipId); emit removeInvalidClip(info.clipId, info.replaceProducer); continue; } 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! path.prepend(QStringLiteral("xml:")); } else { path.prepend(QStringLiteral("consumer:")); // This is currently crashing so I guess we'd better reject it for now m_processingClipId.removeAll(info.clipId); emit removeInvalidClip(info.clipId, info.replaceProducer, i18n("Cannot import playlists with different profile.")); continue; } pCore->getCurrentProfile()->set_explicit(1); producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); - } else if (type == SlideShow) { + } else if (type == ClipType::SlideShow) { producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); } else if (!url.isValid()) { // WARNING: when is this case used? Not sure it is working.. JBM/ QDomDocument doc; QDomElement mlt = doc.createElement(QStringLiteral("mlt")); QDomElement play = doc.createElement(QStringLiteral("playlist")); play.setAttribute(QStringLiteral("id"), QStringLiteral("playlist0")); doc.appendChild(mlt); mlt.appendChild(play); play.appendChild(doc.importNode(info.xml, true)); QDomElement tractor = doc.createElement(QStringLiteral("tractor")); tractor.setAttribute(QStringLiteral("id"), QStringLiteral("tractor0")); QDomElement track = doc.createElement(QStringLiteral("track")); track.setAttribute(QStringLiteral("producer"), QStringLiteral("playlist0")); tractor.appendChild(track); mlt.appendChild(tractor); producer = std::make_shared(pCore->getCurrentProfile()->profile(), "xml-string", doc.toString().toUtf8().constData()); } else { producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, path.toUtf8().constData()); if (producer->is_valid() && info.xml.hasAttribute(QStringLiteral("checkProfile")) && producer->get_int("video_index") > -1) { // Check if clip profile matches QString service = 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 = producer->get_int("meta.media.width"); int height = 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(); // delete producer; // m_processingClipId.removeAll(info.clipId); info.xml.removeAttribute(QStringLiteral("checkProfile")); emit switchProfile(projectProfile, info.clipId, info.xml); } else { // Very small image, we probably don't want to use this as profile } } else if (service.contains(QStringLiteral("avformat"))) { auto *blankProfile = new Mlt::Profile(); blankProfile->set_explicit(0); if (KdenliveSettings::gpu_accel()) { Clip clp(*producer); Mlt::Producer *glProd = clp.softClone(ClipController::getPassPropertiesList()); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); glProd->attach(scaler); glProd->attach(converter); blankProfile->from_producer(*glProd); delete glProd; } else { blankProfile->from_producer(*producer); } std::unique_ptr clipProfile(new ProfileParam(blankProfile)); std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); clipProfile->adjustWidth(); if (clipProfile != projectProfile) { // Profiles do not match, propose profile adjustment // delete producer; delete blankProfile; // m_processingClipId.removeAll(info.clipId); info.xml.removeAttribute(QStringLiteral("checkProfile")); emit switchProfile(clipProfile, info.clipId, info.xml); } else if (KdenliveSettings::default_profile().isEmpty()) { // Confirm default project format KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); } } } } if (!producer || producer->is_blank() || !producer->is_valid()) { qCDebug(KDENLIVE_LOG) << " / / / / / / / / ERROR / / / / // CANNOT LOAD PRODUCER: " << path; m_processingClipId.removeAll(info.clipId); if (proxyProducer) { // Proxy file is corrupted emit removeInvalidProxy(info.clipId, false); } else { emit removeInvalidClip(info.clipId, info.replaceProducer); } continue; } // Pass useful properties processProducerProperties(producer.get(), info.xml); QString clipName = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { producer->set("kdenlive:clipname", clipName.toUtf8().constData()); } QString groupId = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:folderid")); if (!groupId.isEmpty()) { producer->set("kdenlive:folderid", groupId.toUtf8().constData()); } if (proxyProducer && info.xml.hasAttribute(QStringLiteral("proxy_out"))) { producer->set("length", info.xml.attribute(QStringLiteral("proxy_out")).toInt() + 1); producer->set("out", info.xml.attribute(QStringLiteral("proxy_out")).toInt()); if (producer->get_out() != info.xml.attribute(QStringLiteral("proxy_out")).toInt()) { // Proxy file length is different than original clip length, this will corrupt project so disable this proxy clip qCDebug(KDENLIVE_LOG) << "/ // PROXY LENGTH MISMATCH, DELETE PRODUCER"; m_processingClipId.removeAll(info.clipId); emit removeInvalidProxy(info.clipId, true); continue; } } // TODO: handle forced properties /*if (info.xml.hasAttribute("force_aspect_ratio")) { double aspect = info.xml.attribute("force_aspect_ratio").toDouble(); if (aspect > 0) producer->set("force_aspect_ratio", aspect); } if (info.xml.hasAttribute("force_aspect_num") && info.xml.hasAttribute("force_aspect_den")) { int width = info.xml.attribute("frame_size").section('x', 0, 0).toInt(); int height = info.xml.attribute("frame_size").section('x', 1, 1).toInt(); int aspectNumerator = info.xml.attribute("force_aspect_num").toInt(); int aspectDenominator = info.xml.attribute("force_aspect_den").toInt(); if (aspectDenominator != 0 && width != 0) producer->set("force_aspect_ratio", double(height) * aspectNumerator / aspectDenominator / width); } if (info.xml.hasAttribute("force_fps")) { double fps = info.xml.attribute("force_fps").toDouble(); if (fps > 0) producer->set("force_fps", fps); } if (info.xml.hasAttribute("force_progressive")) { bool ok; int progressive = info.xml.attribute("force_progressive").toInt(&ok); if (ok) producer->set("force_progressive", progressive); } if (info.xml.hasAttribute("force_tff")) { bool ok; int fieldOrder = info.xml.attribute("force_tff").toInt(&ok); if (ok) producer->set("force_tff", fieldOrder); } if (info.xml.hasAttribute("threads")) { int threads = info.xml.attribute("threads").toInt(); if (threads != 1) producer->set("threads", threads); } if (info.xml.hasAttribute("video_index")) { int vindex = info.xml.attribute("video_index").toInt(); if (vindex != 0) producer->set("video_index", vindex); } if (info.xml.hasAttribute("audio_index")) { int aindex = info.xml.attribute("audio_index").toInt(); if (aindex != 0) producer->set("audio_index", aindex); } if (info.xml.hasAttribute("force_colorspace")) { int colorspace = info.xml.attribute("force_colorspace").toInt(); if (colorspace != 0) producer->set("force_colorspace", colorspace); } if (info.xml.hasAttribute("full_luma")) { int full_luma = info.xml.attribute("full_luma").toInt(); if (full_luma != 0) producer->set("set.force_full_luma", full_luma); }*/ int clipOut = 0; int duration = 0; if (info.xml.hasAttribute(QStringLiteral("out"))) { clipOut = info.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 == Color || type == Text || type == TextTemplate || type == QText || type == Image || type == SlideShow) { + if (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || type == ClipType::SlideShow) { int length; if (info.xml.hasAttribute(QStringLiteral("length"))) { length = info.xml.attribute(QStringLiteral("length")).toInt(); clipOut = qMax(1, length - 1); } else { length = EffectsList::property(info.xml, QStringLiteral("length")).toInt(); clipOut -= info.xml.attribute(QStringLiteral("in")).toInt(); if (length < clipOut) { length = clipOut == 1 ? 1 : clipOut + 1; } } // Pass duration if it was forced if (info.xml.hasAttribute(QStringLiteral("duration"))) { duration = info.xml.attribute(QStringLiteral("duration")).toInt(); if (length < duration) { length = duration; if (clipOut > 0) { clipOut = length - 1; } } } if (duration == 0) { duration = length; } producer->set("length", length); int kdenlive_duration = EffectsList::property(info.xml, QStringLiteral("kdenlive:duration")).toInt(); producer->set("kdenlive:duration", kdenlive_duration > 0 ? kdenlive_duration : length); } if (clipOut > 0) { producer->set_in_and_out(info.xml.attribute(QStringLiteral("in")).toInt(), clipOut); } if (info.xml.hasAttribute(QStringLiteral("templatetext"))) { producer->set("templatetext", info.xml.attribute(QStringLiteral("templatetext")).toUtf8().constData()); } int fullWidth = info.imageHeight * pCore->getCurrentDar() + 0.5; int frameNumber = ProjectClip::getXmlProperty(info.xml, QStringLiteral("kdenlive:thumbnailFrame"), QStringLiteral("-1")).toInt(); producer->set("kdenlive:id", info.clipId.toUtf8().constData()); qDebug()<<" * * * ** * * *REQUEST CLIP RELOAD: "< glProd( clp.softClone(ClipController::getPassPropertiesList())); if (frameNumber > 0) { glProd->seek(frameNumber); } Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); glProd->attach(scaler); glProd->attach(converter); frame = glProd->get_frame(); if ((frame != nullptr) && frame->is_valid()) { img = KThumb::getFrame(frame, fullWidth, info.imageHeight); emit replyGetImage(info.clipId, img); } } else { if (frameNumber > 0) { producer->seek(frameNumber); } frame = producer->get_frame(); if ((frame != nullptr) && frame->is_valid()) { img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); emit replyGetImage(info.clipId, img); } } if (frame) { delete frame; } // replace clip m_processingClipId.removeAll(info.clipId); // Store original properties in a kdenlive: prefixed format QDomNodeList props = info.xml.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { QDomElement e = props.at(i).toElement(); QString name = e.attribute(QStringLiteral("name")); if (name.startsWith(QLatin1String("meta."))) { name.prepend(QStringLiteral("kdenlive:")); producer->set(name.toUtf8().constData(), e.firstChild().nodeValue().toUtf8().constData()); } } QMetaObject::invokeMethod(m_binController.get(), "prepareTimelineReplacement", Qt::QueuedConnection, Q_ARG(const requestClipInfo &, info), Q_ARG(const std::shared_ptr &, producer)); continue; } stringMap filePropertyMap; stringMap metadataPropertyMap; char property[200]; if (frameNumber > 0) { producer->seek(frameNumber); } duration = duration > 0 ? duration : producer->get_playtime(); // qCDebug(KDENLIVE_LOG) << "/////// PRODUCER: " << url.path() << " IS: " << producer->get_playtime(); - if (type == SlideShow) { + if (type == ClipType::SlideShow) { int ttl = EffectsList::property(info.xml, QStringLiteral("ttl")).toInt(); QString anim = EffectsList::property(info.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()) { producer->attach(*blur); } } filter->set("transition.geometry", geometry.toUtf8().data()); filter->set("transition.cycle", cycle); producer->attach(*filter); } } } QString fade = EffectsList::property(info.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(info.xml, QStringLiteral("luma_duration")); QString luma_file = EffectsList::property(info.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(info.xml, QStringLiteral("softness")); if (!softness.isEmpty()) { int soft = softness.toInt(); filter->set("luma.softness", (double)soft / 100.0); } } producer->attach(*filter); } } QString crop = EffectsList::property(info.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); producer->attach(*filter); } } } int vindex = -1; const QString mltService = producer->get("mlt_service"); if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { // MLT playlist, create producer with blank profile to get real profile info if (path.startsWith(QLatin1String("consumer:"))) { path = "xml:" + path.section(QLatin1Char(':'), 1); } Mlt::Profile original_profile; Mlt::Producer *tmpProd = new Mlt::Producer(original_profile, nullptr, path.toUtf8().constData()); original_profile.set_explicit(1); filePropertyMap[QStringLiteral("progressive")] = QString::number(static_cast(original_profile.progressive())); filePropertyMap[QStringLiteral("colorspace")] = QString::number(original_profile.colorspace()); filePropertyMap[QStringLiteral("fps")] = QString::number(original_profile.fps()); filePropertyMap[QStringLiteral("aspect_ratio")] = QString::number(original_profile.sar()); double originalFps = original_profile.fps(); if (originalFps > 0 && qAbs(originalFps - pCore->getCurrentFps()) > 1e-4) { // Warning, MLT detects an incorrect length in producer consumer when producer's fps != project's fps // TODO: report bug to MLT delete tmpProd; tmpProd = new Mlt::Producer(original_profile, nullptr, path.toUtf8().constData()); int originalLength = tmpProd->get_length(); int fixedLength = (int)(originalLength * pCore->getCurrentFps() / originalFps); producer->set("length", fixedLength); producer->set("out", fixedLength - 1); } delete tmpProd; } else if (mltService == QLatin1String("avformat")) { // Get frame rate vindex = producer->get_int("video_index"); // List streams int streams = producer->get_int("meta.media.nb_streams"); QList audio_list; QList video_list; for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString stype = producer->get(propertyName.data()); if (stype == QLatin1String("audio")) { audio_list.append(i); } else if (stype == QLatin1String("video")) { video_list.append(i); } } int bypass = EffectsList::property(info.xml, QStringLiteral("bypassDuplicate")).toInt(); if (!info.xml.hasAttribute(QStringLiteral("video_index")) && video_list.count() > 1 && bypass != 1) { // Clip has more than one video stream, ask which one should be used QMap data; if (info.xml.hasAttribute(QStringLiteral("group"))) { data.insert(QStringLiteral("group"), info.xml.attribute(QStringLiteral("group"))); } if (info.xml.hasAttribute(QStringLiteral("groupId"))) { data.insert(QStringLiteral("groupId"), info.xml.attribute(QStringLiteral("groupId"))); } emit multiStreamFound(path, audio_list, video_list, data); // Force video index so that when reloading the clip we don't ask again for other streams filePropertyMap[QStringLiteral("video_index")] = QString::number(vindex); } if (vindex > -1) { snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); double fps = producer->get_double(property); if (fps > 0) { filePropertyMap[QStringLiteral("fps")] = locale.toString(fps); } } if (!filePropertyMap.contains(QStringLiteral("fps"))) { if (producer->get_double("meta.media.frame_rate_den") > 0) { filePropertyMap[QStringLiteral("fps")] = locale.toString(producer->get_double("meta.media.frame_rate_num") / producer->get_double("meta.media.frame_rate_den")); } else { double fps = producer->get_double("source_fps"); if (fps > 0) { filePropertyMap[QStringLiteral("fps")] = locale.toString(fps); } } } } - if (!filePropertyMap.contains(QStringLiteral("fps")) && type == Unknown) { + if (!filePropertyMap.contains(QStringLiteral("fps")) && type == ClipType::Unknown) { // something wrong, maybe audio file with embedded image QMimeDatabase db; QString mime = db.mimeTypeForFile(path).name(); if (mime.startsWith(QLatin1String("audio"))) { producer->set("video_index", -1); vindex = -1; } } Mlt::Frame *frame = producer->get_frame(); if ((frame != nullptr) && frame->is_valid()) { if (!mltService.contains(QStringLiteral("avformat"))) { // Fetch thumbnail QImage img; if (KdenliveSettings::gpu_accel()) { delete frame; Clip clp(*producer); Mlt::Producer *glProd = clp.softClone(ClipController::getPassPropertiesList()); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); glProd->attach(scaler); glProd->attach(converter); frame = glProd->get_frame(); img = KThumb::getFrame(frame, fullWidth, info.imageHeight); delete glProd; } else { img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); } emit replyGetImage(info.clipId, img); } else { filePropertyMap[QStringLiteral("frame_size")] = QString::number(frame->get_int("width")) + QLatin1Char('x') + QString::number(frame->get_int("height")); int af = frame->get_int("audio_frequency"); int ac = frame->get_int("audio_channels"); // keep for compatibility with MLT <= 0.8.6 if (af == 0) { af = frame->get_int("frequency"); } if (ac == 0) { ac = frame->get_int("channels"); } if (af > 0) { filePropertyMap[QStringLiteral("frequency")] = QString::number(af); } if (ac > 0) { filePropertyMap[QStringLiteral("channels")] = QString::number(ac); } if (!filePropertyMap.contains(QStringLiteral("aspect_ratio"))) { filePropertyMap[QStringLiteral("aspect_ratio")] = frame->get("aspect_ratio"); } if (frame->get_int("test_image") == 0 && vindex != -1) { if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { filePropertyMap[QStringLiteral("type")] = QStringLiteral("playlist"); metadataPropertyMap[QStringLiteral("comment")] = QString::fromUtf8(producer->get("title")); } else if (mlt_frame_is_test_audio(frame->get_frame()) == 0) { filePropertyMap[QStringLiteral("type")] = QStringLiteral("av"); } else { filePropertyMap[QStringLiteral("type")] = QStringLiteral("video"); } // Check if we are using GPU accel, then we need to use alternate producer std::shared_ptr tmpProd; if (KdenliveSettings::gpu_accel()) { delete frame; Clip clp(*producer); tmpProd = std::shared_ptr(clp.softClone(ClipController::getPassPropertiesList())); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); frame = tmpProd->get_frame(); } else { tmpProd = producer; } QImage img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); if (frameNumber == -1) { // No user specipied frame, look for best one int variance = (int)KThumb::imageVariance(img); if (variance < 6) { // Thumbnail is not interesting (for example all black, seek to fetch better thumb delete frame; frameNumber = duration > 100 ? 100 : duration / 2; tmpProd->seek(frameNumber); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, fullWidth, info.imageHeight, forceThumbScale); } } if (frameNumber > -1) { filePropertyMap[QStringLiteral("thumbnailFrame")] = QString::number(frameNumber); } emit replyGetImage(info.clipId, img); } else if (frame->get_int("test_audio") == 0) { filePropertyMap[QStringLiteral("type")] = QStringLiteral("audio"); } delete frame; if (vindex > -1) { /*if (context->duration == AV_NOPTS_VALUE) { //qCDebug(KDENLIVE_LOG) << " / / / / / / / /ERROR / / / CLIP HAS UNKNOWN DURATION"; emit removeInvalidClip(clipId); delete producer; return; }*/ // Get the video_index int video_max = 0; int default_audio = producer->get_int("audio_index"); int audio_max = 0; int scan = producer->get_int("meta.media.progressive"); filePropertyMap[QStringLiteral("progressive")] = QString::number(scan); // Find maximum stream index values for (int ix = 0; ix < producer->get_int("meta.media.nb_streams"); ++ix) { snprintf(property, sizeof(property), "meta.media.%d.stream.type", ix); QString stype = producer->get(property); if (stype == QLatin1String("video")) { video_max = ix; } else if (stype == QLatin1String("audio")) { audio_max = ix; } } filePropertyMap[QStringLiteral("default_video")] = QString::number(vindex); filePropertyMap[QStringLiteral("video_max")] = QString::number(video_max); filePropertyMap[QStringLiteral("default_audio")] = QString::number(default_audio); filePropertyMap[QStringLiteral("audio_max")] = QString::number(audio_max); snprintf(property, sizeof(property), "meta.media.%d.codec.long_name", vindex); if (producer->get(property)) { filePropertyMap[QStringLiteral("videocodec")] = producer->get(property); } snprintf(property, sizeof(property), "meta.media.%d.codec.name", vindex); if (producer->get(property)) { filePropertyMap[QStringLiteral("videocodecid")] = producer->get(property); } QString query; query = QStringLiteral("meta.media.%1.codec.pix_fmt").arg(vindex); filePropertyMap[QStringLiteral("pix_fmt")] = producer->get(query.toUtf8().constData()); filePropertyMap[QStringLiteral("colorspace")] = producer->get("meta.media.colorspace"); } else { qCDebug(KDENLIVE_LOG) << " / / / / /WARNING, VIDEO CONTEXT IS nullptr!!!!!!!!!!!!!!"; } if (producer->get_int("audio_index") > -1) { // Get the audio_index int index = producer->get_int("audio_index"); snprintf(property, sizeof(property), "meta.media.%d.codec.long_name", index); if (producer->get(property)) { filePropertyMap[QStringLiteral("audiocodec")] = producer->get(property); } else { snprintf(property, sizeof(property), "meta.media.%d.codec.name", index); if (producer->get(property)) { filePropertyMap[QStringLiteral("audiocodec")] = producer->get(property); } } } producer->set("mlt_service", "avformat-novalidate"); } } // metadata Mlt::Properties metadata; metadata.pass_values(*producer, "meta.attr."); int count = metadata.count(); for (int i = 0; i < count; i++) { QString name = metadata.get_name(i); QString value = QString::fromUtf8(metadata.get(i)); if (name.endsWith(QLatin1String(".markup")) && !value.isEmpty()) { metadataPropertyMap[name.section(QLatin1Char('.'), 0, -2)] = value; } } producer->seek(0); emit gotFileProperties(info, producer); m_processingClipId.removeAll(info.clipId); } } void ProducerQueue::abortOperations() { m_infoMutex.lock(); m_requestList.clear(); m_infoMutex.unlock(); m_infoThread.waitForFinished(); } ClipType ProducerQueue::getTypeForService(const QString &id, const QString &path) const { if (id.isEmpty()) { QString ext = path.section(QLatin1Char('.'), -1); if (ext == QLatin1String("mlt") || ext == QLatin1String("kdenlive")) { - return Playlist; + return ClipType::Playlist; } - return Unknown; + return ClipType::Unknown; } if (id == QLatin1String("color") || id == QLatin1String("colour")) { - return Color; + return ClipType::Color; } if (id == QLatin1String("kdenlivetitle")) { - return Text; + return ClipType::Text; } if (id == QLatin1String("qtext")) { - return QText; + return ClipType::QText; } if (id == QLatin1String("xml") || id == QLatin1String("consumer")) { - return Playlist; + return ClipType::Playlist; } if (id == QLatin1String("webvfx")) { - return WebVfx; + return ClipType::WebVfx; } - return Unknown; + return ClipType::Unknown; } void ProducerQueue::processProducerProperties(Mlt::Producer *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()); } } } void ProducerQueue::slotMultiStreamProducerFound(const QString &path, const QList &audio_list, const QList &video_list, stringMap data) { if (KdenliveSettings::automultistreams()) { for (int i = 1; i < video_list.count(); ++i) { int vindex = video_list.at(i); int aindex = 0; if (i <= audio_list.count() - 1) { aindex = audio_list.at(i); } data.insert(QStringLiteral("video_index"), QString::number(vindex)); data.insert(QStringLiteral("audio_index"), QString::number(aindex)); data.insert(QStringLiteral("bypassDuplicate"), QStringLiteral("1")); emit addClip(path, data); } return; } int width = 60.0 * pCore->getCurrentDar(); if (width % 2 == 1) { width++; } QPointer dialog = new QDialog(qApp->activeWindow()); dialog->setWindowTitle(QStringLiteral("Multi Stream Clip")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(dialog); 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", path), 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 < video_list.count(); ++j) { Mlt::Producer multiprod(pCore->getCurrentProfile()->profile(), path.toUtf8().constData()); multiprod.set("video_index", video_list.at(j)); QImage thumb = KThumb::getFrame(&multiprod, 0, width, 60); QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", video_list.at(j)), mainWidget); mainLayout->addWidget(streamFrame); streamFrame->setProperty("vindex", 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 (audio_list.count() > 1) { auto *cb = new KComboBox(mainWidget); mainLayout->addWidget(cb); for (int k = 0; k < audio_list.count(); ++k) { cb->addItem(i18n("Audio stream %1", audio_list.at(k)), audio_list.at(k)); } comboList << cb; cb->setCurrentIndex(qMin(j, 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(); } data.insert(QStringLiteral("kdenlive-force.video_index"), QString::number(vindex)); data.insert(QStringLiteral("kdenlive-force.audio_index"), QString::number(aindex)); data.insert(QStringLiteral("bypassDuplicate"), QStringLiteral("1")); emit addClip(path, data); } } } delete dialog; } diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index 355b916e6..f867eeba0 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2146 +1,2145 @@ /*************************************************************************** * Copyright (C) 2007 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 "monitor.h" #include "monitorcontroller.hpp" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "scopes/monitoraudiolevel.h" #include "timeline/abstractclipitem.h" #include "timeline/clip.h" #include "timeline/transitionhandler.h" #include "timeline2/model/snapmodel.hpp" #include "utils/KoIconUtils.h" #include "transitions/transitionsrepository.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { QDragEnterEvent *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { QDragEnterEvent *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { QDropEvent *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_length(2) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_multitrackView(nullptr) , m_contextMenu(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_lastMonitorSceneType(MonitorSceneDefault) , m_snaps(new SnapModel()) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor, &GLWidget::seekPosition, this, &Monitor::slotSeekPosition, Qt::DirectConnection); m_monitorController = new MonitorController(m_glMonitor); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); auto *monitorEventEater = new QuickMonitorEventEater(this); m_glWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed); connect(m_glMonitor, SIGNAL(mouseSeek(int, int)), this, SLOT(slotMouseSeek(int, int))); connect(m_glMonitor, SIGNAL(monitorPlay()), this, SLOT(slotPlay())); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, SIGNAL(switchFullScreen(bool)), this, SLOT(slotSwitchFullScreen(bool))); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); QWidget *sp1 = new QWidget(this); sp1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(sp1); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, SIGNAL(warningMessage(QString, int, QList)), this, SLOT(warningMessage(QString, int, QList))); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); } if (id != Kdenlive::DvdMonitor) { m_toolbar->addAction(manager->getAction(QStringLiteral("mark_in"))); m_toolbar->addAction(manager->getAction(QStringLiteral("mark_out"))); } m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_backward"))); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_forward"))); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(KoIconUtils::themedIcon(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, SIGNAL(triggered(QAction *)), this, SLOT(slotForceSize(QAction *))); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(100); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(this); menu->addAction(widgetslider); m_audioButton = new QToolButton(this); m_audioButton->setMenu(menu); m_audioButton->setToolTip(i18n("Volume")); m_audioButton->setPopupMode(QToolButton::InstantPopup); QIcon icon; if (KdenliveSettings::volume() == 0) { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-muted")); } else { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); m_toolbar->addWidget(m_audioButton); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); connect(m_glMonitor, &GLWidget::audioSamplesSignal, this, &Monitor::audioSamplesSignal); if (id != Kdenlive::ClipMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), SIGNAL(zoneChanged()), this, SLOT(updateTimelineClipZone())); } else { connect(m_glMonitor->getControllerProxy(), SIGNAL(zoneChanged()), this, SLOT(updateClipZone())); } m_sceneVisibilityAction = new QAction(KoIconUtils::themedIcon(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_zoomVisibilityAction = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-in")), i18n("Zoom"), this); m_zoomVisibilityAction->setCheckable(true); connect(m_zoomVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableSceneZoom); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(m_monitorManager->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); if (m_recManager) { m_toolbar->addAction(m_recManager->switchAction()); } /*QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(spacer);*/ m_toolbar->addSeparator(); int tm = 0; int bm = 0; m_toolbar->getContentsMargins(nullptr, &tm, nullptr, &bm); m_audioMeterWidget = new MonitorAudioLevel(m_glMonitor->profile(), m_toolbar->height() - tm - bm, this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; m_monitorManager->activateMonitor(m_id, false); refreshMonitorIfActive(); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = KoIconUtils::themedIcon(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = KoIconUtils::themedIcon(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = KoIconUtils::themedIcon(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->switchAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->data().toString() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } // TODO: add save zone to timeline monitor when fixed m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { m_contextMenu->addAction(KoIconUtils::themedIcon(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_multitrackView = m_contextMenu->addAction(KoIconUtils::themedIcon(QStringLiteral("view-split-left-right")), i18n("Multitrack view"), this, SIGNAL(multitrackView(bool))); m_multitrackView->setCheckable(true); m_configMenu->addAction(m_multitrackView); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *overlayAudio = m_contextMenu->addAction(QIcon(), i18n("Overlay audio waveform")); overlayAudio->setCheckable(true); connect(overlayAudio, &QAction::toggled, m_glMonitor, &GLWidget::slotSwitchAudioOverlay); overlayAudio->setChecked(KdenliveSettings::displayAudioOverlay()); QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); m_configMenu->addAction(overlayAudio); m_configMenu->addAction(m_zoomVisibilityAction); m_contextMenu->addAction(m_zoomVisibilityAction); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::desktop()->screenGeometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = m_glMonitor->profile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->rulerHeight())); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } QString Monitor::getTimecodeFromFrames(int pos) { return m_monitorManager->timecode().getTimecodeFromFrames(pos); } double Monitor::fps() const { return m_monitorManager->timecode().fps(); } Timecode Monitor::timecode() const { return m_monitorManager->timecode(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(true).frames(m_monitorManager->timecode().fps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(false).frames(m_monitorManager->timecode().fps())); } } int Monitor::position() { return m_glMonitor->getCurrentPos(); } GenTime Monitor::getSnapForPos(bool previous) { QPoint zone = m_glMonitor->getControllerProxy()->zone(); // TODO: move points with the zone m_snaps->addPoint(zone.x()); m_snaps->addPoint(zone.y()); int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return GenTime(frame, pCore->getCurrentFps()); } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y()); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } else { // timeline emit timelineZoneChanged(); } checkOverlay(); } void Monitor::slotSetZoneEnd(bool discardLastFrame) { int pos = m_glMonitor->getCurrentPos() - (discardLastFrame ? 1 : 0); m_glMonitor->getControllerProxy()->setZoneOut(pos); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / m_horizontalScroll->maximum(); float vertical = float(m_verticalScroll->value()) / m_verticalScroll->maximum(); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum(m_glMonitor->profileSize().width() * m_glMonitor->zoom() - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * m_horizontalScroll->maximum())); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = m_glMonitor->profileSize().width() * m_glMonitor->zoom() - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum(m_glMonitor->profileSize().height() * m_glMonitor->zoom() - m_verticalScroll->pageStep()); m_verticalScroll->setValue(qRound(vertical * m_verticalScroll->maximum())); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = m_glMonitor->profileSize().height() * m_glMonitor->zoom() - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (m_glMonitor->zoom() == 1.0f) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? if (!m_glWidget->isFullScreen() && !minimizeOnly) { // Check if we have a multiple monitor setup int monitors = QApplication::desktop()->screenCount(); int screen = -1; if (monitors > 1) { QRect screenres; // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget // int currentScreen = QApplication::desktop()->screenNumber(this); for (int i = 0; screen == -1 && i < QApplication::desktop()->screenCount(); i++) { if (i != QApplication::desktop()->screenNumber(this->parentWidget()->parentWidget())) { screen = i; } } } m_qmlManager->enableAudioThumbs(false); m_glWidget->setParent(QApplication::desktop()->screen(screen)); m_glWidget->move(QApplication::desktop()->screenGeometry(screen).bottomLeft()); m_glWidget->showFullScreen(); } else { m_glWidget->showNormal(); m_qmlManager->enableAudioThumbs(true); QVBoxLayout *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } } void Monitor::reparent() { m_glWidget->setParent(nullptr); m_glWidget->showMinimized(); m_glWidget->showNormal(); QVBoxLayout *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray data; data.append(list.join(QLatin1Char('#')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/producerslist"), data); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->start(Qt::MoveAction); } void Monitor::enterEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(true); QWidget::enterEvent(event); } void Monitor::leaveEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(false); QWidget::leaveEvent(event); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray data; data.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), data); drag->setMimeData(mimeData); drag->start(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), (int)event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_glWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, int modifiers) { if ((modifiers & Qt::ControlModifier) != 0u) { int delta = m_monitorManager->timecode().fps(); if (eventDelta > 0) { delta = 0 - delta; } m_glMonitor->seek(m_glMonitor->getCurrentPos() - delta); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToNextSnap(); } else { emit seekToPreviousSnap(); } } else { if (eventDelta >= 0) { slotForwardOneFrame(); } else { slotRewindOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } emit extractZone(m_controller->AbstractProjectItem::clipId()); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled") ).completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QDir::homePath(); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget (new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); #if KIO_VERSION >= QT_VERSION_CHECK(5,33,0) fileWidget->setSelectedUrl(relativeUrl); #else fileWidget->setSelection(relativeUrl.toString()); #endif KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_monitorController->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_monitorController->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QStringList folderInfo = pCore->bin()->getFolderInfo(); pCore->bin()->droppedUrls(QList() << QUrl::fromLocalFile(selectedFile), folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->seek(pos); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } QPoint zone = m_glMonitor->getControllerProxy()->zone(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, m_monitorManager->timecode().fps()), &found); if (!found) { if (pos == zone.x()) { overlayText = i18n("In Point"); } else if (pos == zone.y()) { overlayText = i18n("Out Point"); } } else { overlayText = marker.comment(); } } m_glMonitor->rootObject()->setProperty("markerText", overlayText); // m_qmlManager->setProperty(QLatin1String("markerText"), overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneOut()); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (speed == 0) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed >= 0) { speed = -1; } else switch ((int)currentspeed) { case -1: speed = -2; break; case -2: speed = -3; break; case -3: speed = -5; break; default: speed = -8; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotForward(double speed) { slotActivateMonitor(); if (speed == 0) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed <= 0) { speed = 1; } else switch ((int)currentspeed) { case 1: speed = 2; break; case 2: speed = 3; break; case 3: speed = 5; break; default: speed = 8; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() - diff); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() + diff); } void Monitor::seekCursor(int pos) { // Deprecated shoud not be used, instead requestSeek /*if (m_ruler->slotNewValue(pos)) { m_timePos->setValue(pos); checkOverlay(pos); if (m_id != Kdenlive::ClipMonitor) { emit renderPosition(pos); } }*/ } void Monitor::adjustRulerSize(int length, std::shared_ptr markerModel) { if (m_controller != nullptr) { QPoint zone = m_controller->zone(); m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } if (length > 0) { m_length = length; } m_timePos->setRange(0, length); connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-muted")); } else { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { slotActivateMonitor(true); } } void Monitor::refreshMonitorIfActive() { if (isActive()) { m_glMonitor->requestRefresh(); } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } void Monitor::updateClipProducer(Mlt::Producer *prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { //TODO Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); //m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } void Monitor::slotOpenClip(std::shared_ptr controller, int in, int out) { if (m_controller) { disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; m_snaps.reset(new SnapModel()); if (controller) { m_controller->getMarkerModel()->registerSnapModel(m_snaps); connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); m_snaps->addPoint(m_controller->frameDuration()); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } m_glMonitor->setRulerInfo(m_controller->frameDuration(), controller->getMarkerModel()); updateMarkers(); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_glMonitor->setProducer(m_controller->originalProducer().get(), isActive(), in); m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; - emit requestAudioThumb(controller->AbstractProjectItem::clipId()); // hasEffects = controller->hasEffects(); } else { m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; } slotActivateMonitor(); checkOverlay(); } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->AbstractProjectItem::clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { //TODO m_glMonitor->initializeGL(); //render->loadUrl(file); } void Monitor::slotSaveZone() { // TODO? or deprecate // render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone()); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { //TODO or deprecate m_timePos->updateTimeCode(tc); if (true) { return; } slotActivateMonitor(); //render->prepareProfileReset(tc.fps()); if (m_multitrackView) { m_multitrackView->setChecked(false); } // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(m_monitorManager->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); double fps = m_monitorManager->timecode().fps(); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } /*void Monitor::saveSceneList(const QString &path, const QDomElement &info) { if (render == nullptr) return; render->saveSceneList(path, info); }*/ const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone() { if (m_controller == nullptr) { return; } m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } void Monitor::updateTimelineClipZone() { emit zoneUpdated(m_glMonitor->getControllerProxy()->zone()); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { if (m_controller == nullptr) { return QPoint(); } return m_controller->zone(); } void Monitor::slotEnableSceneZoom(bool enable) { m_qmlManager->setProperty(QStringLiteral("showToolbar"), enable); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit seekPosition(m_glMonitor->getCurrentPos()); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QRect(); } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } if (volume == 0) { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-muted")); } else { icon = KoIconUtils::themedIcon(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { m_monitorManager->frameDisplayed(frame); int position = frame.get_position(); if (!m_glMonitor->checkFrameNumber(position)) { m_playAction->setActive(false); } /* else if (position >= m_length) { m_playAction->setActive(false); }*/ } void Monitor::checkDrops(int dropped) { if (m_droppedTimer.isValid()) { if (m_droppedTimer.hasExpired(1000)) { m_droppedTimer.invalidate(); double fps = m_monitorManager->timecode().fps(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } else { m_glMonitor->resetDrops(); fps -= dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); m_droppedTimer.start(); } } } else if (dropped > 0) { // Start m_dropTimer m_glMonitor->resetDrops(); m_droppedTimer.start(); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { QString url = m_monitorManager->getCacheFolder(CacheThumbs) .absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(m_monitorManager->timecode().fps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } return QString(); } const QString Monitor::projectFolder() const { return m_monitorManager->getProjectFolder(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); 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 = KoIconUtils::themedIcon(ic.name()); m->setIcon(newIcon); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); slotActivateMonitor(true); } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); delete m_splitEffect; m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); slotActivateMonitor(true); return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { //TODO m_glMonitor->setProducer(m_controller->originalProducer().get(), isActive(), position()); delete m_splitEffect; m_splitProducer = nullptr; m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { qDebug()<<"// BUILDING SPLIT EFFECT!!!"; m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { delete m_splitEffect; pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(*profile()); //TODO: remove usage of Clip class Clip clp(*original); Mlt::Producer *clone = clp.clone(); Clip clp2(*clone); clp2.deleteEffects(); trac.set_track(*original, 0); trac.set_track(*clone, 1); clone->attach(*m_splitEffect); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete clone; delete original; m_splitProducer = new Mlt::Producer(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type) { if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes type = MonitorSceneDefault; } double ratio = (double) m_glMonitor->profileSize().width() / (int) (m_glMonitor->profileSize().height() * m_glMonitor->profile()->dar() + 0.5); m_qmlManager->setScene(m_id, type, m_glMonitor->profileSize(), ratio, m_glMonitor->displayRect(), m_glMonitor->zoom()); QQuickItem *root = m_glMonitor->rootObject(); root->setProperty("showToolbar", m_zoomVisibilityAction->isChecked()); connectQmlToolbar(root); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: QObject::connect(root, SIGNAL(addKeyframe()), this, SIGNAL(addKeyframe()), Qt::UniqueConnection); QObject::connect(root, SIGNAL(seekToKeyframe()), this, SLOT(slotSeekToKeyFrame()), Qt::UniqueConnection); QObject::connect(root, SIGNAL(toolBarChanged(bool)), m_zoomVisibilityAction, SLOT(setChecked(bool)), Qt::UniqueConnection); break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(m_monitorManager->timecode().fps(), 'g', 2)); } void Monitor::connectQmlToolbar(QQuickItem *root) { QObject *button = root->findChild(QStringLiteral("fullScreen")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SLOT(slotSwitchFullScreen()), Qt::UniqueConnection); } // Normal monitor toolbar button = root->findChild(QStringLiteral("nextSnap")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(seekToNextSnap()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("prevSnap")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(seekToPreviousSnap()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("addMarker")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(addMarker()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("removeMarker")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(deleteMarker()), Qt::UniqueConnection); } // Effect monitor toolbar button = root->findChild(QStringLiteral("nextKeyframe")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(seekToNextKeyframe()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("prevKeyframe")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(seekToPreviousKeyframe()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("addKeyframe")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(addKeyframe()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("removeKeyframe")); if (button) { QObject::connect(button, SIGNAL(clicked()), this, SIGNAL(deleteKeyframe()), Qt::UniqueConnection); } button = root->findChild(QStringLiteral("zoomSlider")); if (button) { QObject::connect(button, SIGNAL(zoomChanged(double)), m_glMonitor, SLOT(slotZoomScene(double)), Qt::UniqueConnection); } } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { QRect r = m_glMonitor->rect(); double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = 0.5 - ((root->property("splitterPos").toInt() - r.left() - r.width() / 2.0) / (double)r.width() / 2.0) / 0.75; // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", percent); } m_glMonitor->refresh(); } Mlt::Profile *Monitor::profile() { return m_glMonitor->profile(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } bool Monitor::startCapture(const QString ¶ms, const QString &path, Mlt::Producer *p) { //TODO m_controller = nullptr; if (false) {//render->updateProducer(p)) { m_glMonitor->reconfigureMulti(params, path, p->profile()); return true; } return false; } bool Monitor::stopCapture() { m_glMonitor->stopCapture(); slotOpenClip(nullptr); m_glMonitor->reconfigure(profile()); return true; } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } void Monitor::prepareAudioThumb(int channels, QVariantList &audioCache) { m_glMonitor->setAudioThumb(channels, audioCache); } void Monitor::slotUpdateQmlTimecode(const QString &tc) { checkDrops(m_glMonitor->droppedFrames()); m_glMonitor->rootObject()->setProperty("timecode", tc); } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); m_glMonitor->rootObject()->setProperty("showFps", currentOverlay & 0x20); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); bool showTimecodeRelatedInfo = ((currentOverlay & 0x02) != 0) || ((currentOverlay & 0x20) != 0); m_timePos->sendTimecode(showTimecodeRelatedInfo); if (showTimecodeRelatedInfo) { connect(m_timePos, &TimecodeDisplay::emitTimeCode, this, &Monitor::slotUpdateQmlTimecode, Qt::UniqueConnection); } else { disconnect(m_timePos, &TimecodeDisplay::emitTimeCode, this, &Monitor::slotUpdateQmlTimecode); } m_glMonitor->rootObject()->setProperty("showSafezone", currentOverlay & 0x08); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::requestSeek(int pos) { m_glMonitor->seek(pos); } void Monitor::setProducer(Mlt::Producer *producer, int pos) { m_glMonitor->setProducer(producer, isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } int Monitor::duration() const { return m_length; } void Monitor::slotSeekPosition(int pos) { emit seekPosition(pos); m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->seek(0); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->seek(m_glMonitor->duration()); } diff --git a/src/monitor/monitor.h b/src/monitor/monitor.h index c58dade43..7a42c95e4 100644 --- a/src/monitor/monitor.h +++ b/src/monitor/monitor.h @@ -1,374 +1,373 @@ /*************************************************************************** * Copyright (C) 2007 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 MONITOR_H #define MONITOR_H #include "abstractmonitor.h" #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "effectslist/effectslist.h" #include "gentime.h" #include "scopes/sharedframe.h" #include "timecodedisplay.h" #include #include #include #include #include #include class SnapModel; class ProjectClip; class MonitorManager; class QSlider; class KDualAction; class KSelectAction; class KMessageWidget; class QQuickItem; class QScrollBar; class RecManager; class QToolButton; class QmlManager; class GLWidget; class MonitorAudioLevel; class MonitorController; namespace Mlt { class Profile; class Filter; } class QuickEventEater : public QObject { Q_OBJECT public: explicit QuickEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void addEffect(const QStringList&); }; class QuickMonitorEventEater : public QObject { Q_OBJECT public: explicit QuickMonitorEventEater(QWidget *parent); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void doKeyPressEvent(QKeyEvent *); }; class Monitor : public AbstractMonitor { Q_OBJECT public: friend class MonitorManager; Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent = nullptr); ~Monitor(); void resetProfile(); void setCustomProfile(const QString &profile, const Timecode &tc); void setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu = nullptr, QAction *loopClip = nullptr); const QString sceneList(const QString &root, const QString &fullPath = QString()); const QString activeClipId(); int position(); void updateTimecodeFormat(); void updateMarkers(); /** @brief Controller for the clip currently displayed (only valid for clip monitor). */ std::shared_ptr currentController() const; /** @brief Add timeline guides to the ruler and context menu */ void setGuides(const QMap &guides); void reloadProducer(const QString &id); /** @brief Reimplemented from QWidget, updates the palette colors. */ void setPalette(const QPalette &p); /** @brief Returns a hh:mm:ss timecode from a frame number. */ QString getTimecodeFromFrames(int pos); /** @brief Returns current project's fps. */ double fps() const; /** @brief Returns current project's timecode. */ Timecode timecode() const; /** @brief Get url for the clip's thumbnail */ QString getMarkerThumb(GenTime pos); /** @brief Get current project's folder */ const QString projectFolder() const; /** @brief Get the project's Mlt profile */ Mlt::Profile *profile(); int getZoneStart(); int getZoneEnd(); void setUpEffectGeometry(const QRect &r, const QVariantList &list = QVariantList(), const QVariantList &types = QVariantList()); /** @brief Set a property on the effect scene */ void setEffectSceneProperty(const QString &name, const QVariant &value); /** @brief Returns effective display size */ QSize profileSize() const; QRect effectRect() const; QVariantList effectPolygon() const; QVariantList effectRoto() const; void setEffectKeyframe(bool enable); void sendFrameForAnalysis(bool analyse); void updateAudioForAnalysis(); void switchMonitorInfo(int code); void switchDropFrames(bool drop); void updateMonitorGamma(); void mute(bool, bool updateIconOnly = false) override; bool startCapture(const QString ¶ms, const QString &path, Mlt::Producer *p); bool stopCapture(); void reparent(); /** @brief Returns the action displaying record toolbar */ QAction *recAction(); void refreshIcons(); /** @brief Send audio thumb data to qml for on monitor display */ void prepareAudioThumb(int channels, QVariantList &audioCache); void connectAudioSpectrum(bool activate); /** @brief Set a property on the Qml scene **/ void setQmlProperty(const QString &name, const QVariant &value); void displayAudioMonitor(bool isActive); /** @brief Prepare split effect from timeline clip producer **/ void activateSplit(); /** @brief Clear monitor display **/ void clearDisplay(); void setProducer(Mlt::Producer *producer, int pos = -1); /** @brief Returns current monitor's duration in frames **/ int duration() const; void reconfigure(); /** @brief Saves current monitor frame to an image file, and add it to project if addToProject is set to true **/ void slotExtractCurrentFrame(QString frameName = QString(), bool addToProject = false); protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; /** @brief Move to another position on mouse wheel event. * * Moves towards the end of the clip/timeline on mouse wheel down/back, the * opposite on mouse wheel up/forward. * Ctrl + wheel moves by a second, without Ctrl it moves by a single frame. */ void wheelEvent(QWheelEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; virtual QStringList mimeTypes() const; MonitorController *m_monitorController; private: std::shared_ptr m_controller; /** @brief The QQuickView that handles our monitor display (video and qml overlay) **/ GLWidget *m_glMonitor; /** @brief Container for our QQuickView monitor display (QQuickView needs to be embedded) **/ QWidget *m_glWidget; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_verticalScroll; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_horizontalScroll; /** @brief Widget holding the window for the QQuickView **/ QWidget *m_videoWidget; /** @brief Manager for qml overlay for the QQuickView **/ QmlManager *m_qmlManager; std::shared_ptr m_snaps; Mlt::Filter *m_splitEffect; Mlt::Producer *m_splitProducer; int m_length; bool m_dragStarted; // TODO: Move capture stuff in own class RecManager *m_recManager; /** @brief The widget showing current time position **/ TimecodeDisplay *m_timePos; KDualAction *m_playAction; KSelectAction *m_forceSize; /** Has to be available so we can enable and disable it. */ QAction *m_loopClipAction; QAction *m_sceneVisibilityAction; QAction *m_zoomVisibilityAction; QAction *m_multitrackView; QMenu *m_contextMenu; QMenu *m_configMenu; QMenu *m_playMenu; QMenu *m_markerMenu; QPoint m_DragStartPosition; /** true if selected clip is transition, false = selected clip is clip. * Necessary because sometimes we get two signals, e.g. we get a clip and we get selected transition = nullptr. */ bool m_loopClipTransition; GenTime getSnapForPos(bool previous); QToolBar *m_toolbar; QToolButton *m_audioButton; QSlider *m_audioSlider; QAction *m_editMarker; KMessageWidget *m_infoMessage; int m_forceSizeFactor; MonitorSceneType m_lastMonitorSceneType; MonitorAudioLevel *m_audioMeterWidget; QElapsedTimer m_droppedTimer; double m_displayedFps; void adjustScrollBars(float horizontal, float vertical); void loadQmlScene(MonitorSceneType type); void updateQmlDisplay(int currentOverlay); /** @brief Connect qml on monitor toolbar buttons */ void connectQmlToolbar(QQuickItem *root); /** @brief Check and display dropped frames */ void checkDrops(int dropped); /** @brief Create temporary Mlt::Tractor holding a clip and it's effectless clone */ void buildSplitEffect(Mlt::Producer *original); private slots: Q_DECL_DEPRECATED void seekCursor(int pos); void slotSetThumbFrame(); void slotSaveZone(); void slotSeek(); void updateClipZone(); void slotGoToMarker(QAction *action); void slotSetVolume(int volume); void slotEditMarker(); void slotExtractCurrentZone(); void onFrameDisplayed(const SharedFrame &frame); void slotStartDrag(); void setZoom(); void slotEnableEffectScene(bool enable); void slotAdjustEffectCompare(); void slotShowMenu(const QPoint pos); void slotForceSize(QAction *a); void slotSeekToKeyFrame(); /** @brief Display a non blocking error message to user **/ void warningMessage(const QString &text, int timeout = 5000, const QList &actions = QList()); void slotLockMonitor(bool lock); void slotAddEffect(const QStringList &effect); void slotSwitchPlay(); void slotEditInlineMarker(); /** @brief Pass keypress event to mainwindow */ void doKeyPressEvent(QKeyEvent *); /** @brief The timecode was updated, refresh qml display */ void slotUpdateQmlTimecode(const QString &tc); /** @brief There was an error initializing Movit */ void gpuError(); void setOffsetX(int x); void setOffsetY(int y); /** @brief Show/hide monitor zoom */ void slotEnableSceneZoom(bool enable); /** @brief Pan monitor view */ void panView(QPoint diff); /** @brief Project monitor zone changed, inform timeline */ void updateTimelineClipZone(); void slotSeekPosition(int); public slots: void slotOpenDvdFile(const QString &); // void slotSetClipProducer(DocClipBase *clip, QPoint zone = QPoint(), bool forceUpdate = false, int position = -1); void updateClipProducer(Mlt::Producer *prod); void updateClipProducer(const QString &playlist); void slotOpenClip(std::shared_ptr controller, int in = -1, int out = -1); void slotRefreshMonitor(bool visible); void slotSeek(int pos); void stop() override; void start() override; void switchPlay(bool play); void slotPlay() override; void pause(); void slotPlayZone(); void slotLoopZone(); /** @brief Loops the selected item (clip or transition). */ void slotLoopClip(); void slotForward(double speed = 0); void slotRewind(double speed = 0); void slotRewindOneFrame(int diff = 1); void slotForwardOneFrame(int diff = 1); void slotStart(); void slotEnd(); void slotSetZoneStart(); void slotSetZoneEnd(bool discardLastFrame = false); void slotZoneStart(); void slotZoneEnd(); void slotLoadClipZone(const QPoint &zone); void slotSeekToNextSnap(); void slotSeekToPreviousSnap(); void adjustRulerSize(int length, std::shared_ptr markerModel = nullptr); void setTimePos(const QString &pos); QPoint getZoneInfo() const; /** @brief Display the on monitor effect scene (to adjust geometry over monitor). */ void slotShowEffectScene(MonitorSceneType sceneType, bool temporary = false); bool effectSceneDisplayed(MonitorSceneType effectType); /** @brief split screen to compare clip with and without effect */ void slotSwitchCompare(bool enable); void slotMouseSeek(int eventDelta, int modifiers) override; void slotSwitchFullScreen(bool minimizeOnly = false) override; /** @brief Display or hide the record toolbar */ void slotSwitchRec(bool enable); /** @brief Request QImage of current frame */ void slotGetCurrentImage(bool request); /** @brief Enable/disable display of monitor's audio levels widget */ void slotSwitchAudioMonitor(); /** @brief Request seeking */ void requestSeek(int pos); /** @brief Check current position to show relevant infos in qml view (markers, zone in/out, etc). */ void checkOverlay(int pos = -1); void refreshMonitorIfActive() override; signals: void seekPosition(int); /** @brief Request a timeline seeking if diff is true, position is a relative offset, otherwise an absolute position */ void seekTimeline(int position); void durationChanged(int); void refreshClipThumbnail(const QString &); void zoneUpdated(const QPoint &); void timelineZoneChanged(); /** @brief Editing transitions / effects over the monitor requires the renderer to send frames as QImage. * This causes a major slowdown, so we only enable it if required */ void requestFrameForAnalysis(bool); /** @brief Request a zone extraction (ffmpeg transcoding). */ void extractZone(const QString &id); void effectChanged(const QRect &); void effectPointsChanged(const QVariantList &); void addKeyframe(); void deleteKeyframe(); void seekToNextKeyframe(); void seekToPreviousKeyframe(); void seekToKeyframe(int); void addClipToProject(const QUrl &); void showConfigDialog(int, int); /** @brief Request display of current bin clip. */ void refreshCurrentClip(); void addEffect(const QStringList &); void addMasterEffect(QString, const QStringList &); void passKeyPress(QKeyEvent *); /** @brief Enable / disable project monitor multitrack view (split view with one track in each quarter). */ void multitrackView(bool); - void requestAudioThumb(const QString &); void timeCodeUpdated(const QString &); void addMarker(); void deleteMarker(bool deleteGuide = true); void seekToPreviousSnap(); void seekToNextSnap(); void createSplitOverlay(Mlt::Filter *); void removeSplitOverlay(); void acceptRipple(bool); void switchTrimMode(int); }; #endif diff --git a/src/project/CMakeLists.txt b/src/project/CMakeLists.txt index 6eeae3b31..ea4693d22 100644 --- a/src/project/CMakeLists.txt +++ b/src/project/CMakeLists.txt @@ -1,13 +1,12 @@ add_subdirectory(dialogs) -add_subdirectory(jobs) set(kdenlive_SRCS ${kdenlive_SRCS} project/clipmanager.cpp project/clipstabilize.cpp project/cliptranscode.cpp project/invaliddialog.cpp project/projectcommands.cpp project/projectmanager.cpp project/effectsettings.cpp project/notesplugin.cpp PARENT_SCOPE) diff --git a/src/project/clipmanager.cpp b/src/project/clipmanager.cpp index f56215193..9183c3b6b 100644 --- a/src/project/clipmanager.cpp +++ b/src/project/clipmanager.cpp @@ -1,263 +1,218 @@ /*************************************************************************** * Copyright (C) 2008 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 "clipmanager.h" #include "bin/bin.h" #include "bin/bincommands.h" #include "core.h" #include "dialogs/slideshowclip.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "project/projectmanager.h" #include "renderer.h" #include "titler/titledocument.h" #include #include #include #include #include #include #include #include #include ClipManager::ClipManager(KdenliveDoc *doc) : QObject() , m_audioThumbsQueue() , m_doc(doc) , m_abortThumb(false) , m_closing(false) , m_abortAudioThumb(false) { } ClipManager::~ClipManager() { m_closing = true; m_abortThumb = true; m_abortAudioThumb = true; m_thumbsThread.waitForFinished(); m_audioThumbsThread.waitForFinished(); m_thumbsMutex.lock(); m_requestedThumbs.clear(); m_audioThumbsQueue.clear(); m_thumbsMutex.unlock(); } void ClipManager::clear() { m_abortThumb = true; m_abortAudioThumb = true; m_thumbsThread.waitForFinished(); m_audioThumbsThread.waitForFinished(); m_thumbsMutex.lock(); m_requestedThumbs.clear(); m_audioThumbsQueue.clear(); m_thumbsMutex.unlock(); m_abortThumb = false; m_abortAudioThumb = false; m_folderList.clear(); m_modifiedClips.clear(); } - void ClipManager::slotRequestThumbs(const QString &id, const QList &frames) { m_thumbsMutex.lock(); for (int frame : frames) { m_requestedThumbs.insertMulti(id, frame); } m_thumbsMutex.unlock(); if (!m_thumbsThread.isRunning() && !m_abortThumb) { m_thumbsThread = QtConcurrent::run(this, &ClipManager::slotGetThumbs); } } void ClipManager::stopThumbs(const QString &id) { if (m_closing || (m_requestedThumbs.isEmpty() && m_processingThumbId != id && m_audioThumbsQueue.isEmpty() && m_processingAudioThumbId != id)) { return; } // Abort video thumbs for this clip m_abortThumb = true; m_thumbsThread.waitForFinished(); m_thumbsMutex.lock(); m_requestedThumbs.remove(id); m_audioThumbsQueue.removeAll(id); m_thumbsMutex.unlock(); m_abortThumb = false; // Abort audio thumbs for this clip if (m_processingAudioThumbId == id) { m_abortAudioThumb = true; m_audioThumbsThread.waitForFinished(); m_abortAudioThumb = false; } if (!m_thumbsThread.isRunning() && !m_requestedThumbs.isEmpty()) { m_thumbsThread = QtConcurrent::run(this, &ClipManager::slotGetThumbs); } } void ClipManager::slotGetThumbs() { QMap::const_iterator i; while (!m_requestedThumbs.isEmpty() && !m_abortThumb) { m_thumbsMutex.lock(); i = m_requestedThumbs.constBegin(); m_processingThumbId = i.key(); QList values = m_requestedThumbs.values(m_processingThumbId); m_requestedThumbs.remove(m_processingThumbId); // TODO int thumbType = 0; // 0 = timeline thumb, 1 = project clip zone thumb, 2 = clip properties thumb if (m_processingThumbId.startsWith(QLatin1String("?"))) { // if id starts with ?, it means the request comes from a clip property widget // TODO thumbType = 2; m_processingThumbId.remove(0, 1); } if (m_processingThumbId.startsWith(QLatin1String("#"))) { // if id starts with #, it means the request comes from project tree // TODO thumbType = 1; m_processingThumbId.remove(0, 1); } m_thumbsMutex.unlock(); qSort(values); // TODO /* DocClipBase *clip = getClipById(m_processingThumbId); if (!clip) continue; max = m_requestedThumbs.size() + values.count(); // keep in sync with declaration un projectitem.cpp and subprojectitem.cpp int minHeight = qMax(38, QFontMetrics(QApplication::font()).lineSpacing() * 2); while (!values.isEmpty() && clip->thumbProducer() && !m_abortThumb) { int pos = values.takeFirst(); switch (thumbType) { case 1: clip->thumbProducer()->getGenericThumb(pos, minHeight, thumbType); break; case 2: clip->thumbProducer()->getGenericThumb(pos, 180, thumbType); break; default: clip->thumbProducer()->getThumb(pos); } done++; if (max > 3) emit displayMessage(i18n("Loading thumbnails"), 100 * done / max); } */ } m_processingThumbId.clear(); emit displayMessage(QString(), -1); } - void ClipManager::slotAddCopiedClip(KIO::Job *, const QUrl &, const QUrl &dst) { pCore->bin()->droppedUrls(QList() << dst); } -void ClipManager::slotAddTextTemplateClip(const QString &titleName, const QUrl &path, const QString &group, const QString &groupId) -{ - QDomDocument doc; - QDomElement prod = doc.createElement(QStringLiteral("producer")); - doc.appendChild(prod); - prod.setAttribute(QStringLiteral("name"), titleName); - prod.setAttribute(QStringLiteral("resource"), path.toLocalFile()); - int id = pCore->bin()->getFreeClipId(); - prod.setAttribute(QStringLiteral("id"), QString::number(id)); - if (!group.isEmpty()) { - prod.setAttribute(QStringLiteral("groupname"), group); - prod.setAttribute(QStringLiteral("groupid"), groupId); - } - prod.setAttribute(QStringLiteral("type"), (int)Text); - prod.setAttribute(QStringLiteral("transparency"), QStringLiteral("1")); - prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); - - int duration = 0; - QDomDocument titledoc; - QFile txtfile(path.toLocalFile()); - if (txtfile.open(QIODevice::ReadOnly) && titledoc.setContent(&txtfile)) { - if (titledoc.documentElement().hasAttribute(QStringLiteral("duration"))) { - duration = titledoc.documentElement().attribute(QStringLiteral("duration")).toInt(); - } else { - // keep some time for backwards compatibility - 26/12/12 - duration = titledoc.documentElement().attribute(QStringLiteral("out")).toInt(); - } - } - txtfile.close(); - - if (duration == 0) { - duration = m_doc->getFramePos(KdenliveSettings::title_duration()); - } - prod.setAttribute(QStringLiteral("duration"), duration - 1); - prod.setAttribute(QStringLiteral("out"), duration - 1); - - AddClipCommand *command = new AddClipCommand(pCore->bin(), doc.documentElement(), QString::number(id), true); - m_doc->commandStack()->push(command); -} - - int ClipManager::lastClipId() const { return pCore->bin()->lastClipId(); } - - /* void ClipManager::slotClipMissing(const QString &path) { qCDebug(KDENLIVE_LOG) << "// CLIP: " << path << " WAS MISSING"; //TODO const QList list = getClipByResource(path); for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip != nullptr) emit missingClip(clip->getId()); } } void ClipManager::slotClipAvailable(const QString &path) { qCDebug(KDENLIVE_LOG) << "// CLIP: " << path << " WAS ADDED"; //TODO const QList list = getClipByResource(path); for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip != nullptr) emit availableClip(clip->getId()); } } */ void ClipManager::projectTreeThumbReady(const QString &id, int frame, const QImage &img, int type) { switch (type) { case 2: emit gotClipPropertyThumbnail(id, img); break; default: emit thumbReady(id, frame, img); } } diff --git a/src/project/clipmanager.h b/src/project/clipmanager.h index b2784a143..7ebf8ed60 100644 --- a/src/project/clipmanager.h +++ b/src/project/clipmanager.h @@ -1,123 +1,122 @@ /*************************************************************************** * Copyright (C) 2008 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 CLIPMANAGER_H #define CLIPMANAGER_H #include #include #include #include #include #include #include "definitions.h" #include "gentime.h" class KdenliveDoc; class AbstractGroupItem; class QUndoCommand; class SolidVolumeInfo { public: QString path; // mount path of volume, with trailing slash QString uuid; // UUID as from Solid QString label; // volume label (think of CDs) bool isRemovable; // may be removed bool isMounted; bool isNull() const { return path.isNull(); } }; namespace Mlt { } /** * @class ClipManager * @brief Takes care of clip operations that might affect timeline and bin */ class ClipManager : public QObject { Q_OBJECT public : explicit ClipManager(KdenliveDoc *doc); virtual ~ClipManager(); - void slotAddTextTemplateClip(const QString &titleName, const QUrl &path, const QString &group, const QString &groupId); int lastClipId() const; /** @brief Prepare deletion of clips and folders from the Bin. */ void clear(); /** @brief remove a clip id from the queue list. */ void stopThumbs(const QString &id); void projectTreeThumbReady(const QString &id, int frame, const QImage &img, int type); public slots: /** @brief Request creation of a clip thumbnail for specified frames. */ void slotRequestThumbs(const QString &id, const QList &frames); private slots: void slotGetThumbs(); /** @brief Clip has been copied, add it now. */ void slotAddCopiedClip(KIO::Job *, const QUrl &, const QUrl &dst); private: // Private attributes /** @brief the list of groups in the document */ QList m_groupsList; QMap m_folderList; QList m_audioThumbsQueue; /** the document undo stack*/ KdenliveDoc *m_doc; /** List of the clip IDs that need to be reloaded after being externally modified */ QMap m_modifiedClips; /** Struct containing the list of clip thumbnails to request (clip id and frames) */ QMap m_requestedThumbs; QMutex m_thumbsMutex; QFuture m_thumbsThread; /** @brief The id of currently processed clip for thumbs creation. */ QString m_processingThumbId; /** @brief If true, abort processing of clip thumbs before removing a clip. */ bool m_abortThumb; /** @brief We are about to delete the clip producer, stop processing thumbs. */ bool m_closing; QFuture m_audioThumbsThread; /** @brief If true, abort processing of audio thumbs. */ bool m_abortAudioThumb; /** @brief The id of currently processed clip for audio thumbs creation. */ QString m_processingAudioThumbId; QMutex m_groupsMutex; QPoint m_projectTreeThumbSize; signals: void reloadClip(const QString &); void modifiedClip(const QString &); void missingClip(const QString &); void availableClip(const QString &); void checkAllClips(bool displayRatioChanged, bool fpsChanged, const QStringList &brokenClips); void displayMessage(const QString &, int); void thumbReady(const QString &id, int, const QImage &); void gotClipPropertyThumbnail(const QString &id, const QImage &); }; #endif diff --git a/src/project/clipstabilize.cpp b/src/project/clipstabilize.cpp index 53bd405f1..6c905f278 100644 --- a/src/project/clipstabilize.cpp +++ b/src/project/clipstabilize.cpp @@ -1,238 +1,234 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2011 by Marco Gittler (marco@gitma.de) * * * * 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 "clipstabilize.h" +#include "bin/projectclip.h" +#include "bin/projectitemmodel.h" +#include "core.h" #include "widgets/doublewidget.h" #include "widgets/positionwidget.h" #include "kdenlivesettings.h" #include #include #include #include -ClipStabilize::ClipStabilize(const QStringList &urls, const QString &filterName, int out, QWidget *parent) +ClipStabilize::ClipStabilize(const std::vector &binIds, const QString &filterName, int out, QWidget *parent) : QDialog(parent) , m_filtername(filterName) - , m_urls(urls) + , m_binIds(binIds) , vbox(nullptr) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setupUi(this); setWindowTitle(i18n("Stabilize Clip")); - auto_add->setText(i18np("Add clip to project", "Add clips to project", urls.count())); + auto_add->setText(i18np("Add clip to project", "Add clips to project", m_binIds.size())); auto_add->setChecked(KdenliveSettings::add_new_clip()); - //QString stylesheet = EffectStackView2::getStyleSheet(); - //setStyleSheet(stylesheet); + // QString stylesheet = EffectStackView2::getStyleSheet(); + // setStyleSheet(stylesheet); - if (m_urls.count() == 1) { - QString newFile = m_urls.constFirst(); + Q_ASSERT(binIds.size() > 0); + auto firstBinClip = pCore->projectItemModel()->getClipByBinID(m_binIds.front()); + auto firstUrl = firstBinClip->url(); + if (m_binIds.size() == 1) { + QString newFile = firstUrl; newFile.append(QStringLiteral(".mlt")); dest_url->setMode(KFile::File); dest_url->setUrl(QUrl(newFile)); } else { label_dest->setText(i18n("Destination folder")); dest_url->setMode(KFile::Directory | KFile::ExistingOnly); - dest_url->setUrl(QUrl(m_urls.constFirst()).adjusted(QUrl::RemoveFilename)); + dest_url->setUrl(QUrl(firstUrl).adjusted(QUrl::RemoveFilename)); } if (m_filtername == QLatin1String("vidstab") || m_filtername == QLatin1String("videostab2")) { - m_fixedParams.insert(QStringLiteral("algo"), QStringLiteral("1")); - m_fixedParams.insert(QStringLiteral("relative"), QStringLiteral("1")); + m_fixedParams[QStringLiteral("algo")] = QStringLiteral("1"); + m_fixedParams[QStringLiteral("relative")] = QStringLiteral("1"); fillParameters( QStringList() << QStringLiteral("accuracy,type,int,value,8,min,1,max,10,tooltip,Accuracy of Shakiness detection") << QStringLiteral("shakiness,type,int,value,4,min,1,max,10,tooltip,How shaky is the Video") << QStringLiteral("stepsize,type,int,value,6,min,0,max,100,tooltip,Stepsize of Detection process minimum around") << QStringLiteral("mincontrast,type,double,value,0.3,min,0,max,1,factor,1,decimals,2,tooltip,Below this Contrast Field is discarded") << QStringLiteral("smoothing,type,int,value,10,min,0,max,100,tooltip,number of frames for lowpass filtering") << QStringLiteral("maxshift,type,int,value,-1,min,-1,max,1000,tooltip,max number of pixels to shift") << QStringLiteral("maxangle,type,double,value,-1,min,-1,max,3.14,decimals,2,tooltip,max angle to rotate (in rad)") << QStringLiteral("crop,type,bool,value,0,min,0,max,1,tooltip,0 = keep border 1 = black background") << QStringLiteral("zoom,type,int,value,0,min,-500,max,500,tooltip,additional zoom during transform") << QStringLiteral("optzoom,type,bool,value,1,min,0,max,1,tooltip,use optimal zoom (calulated from transforms)") << QStringLiteral("sharpen,type,double,value,0.8,min,0,max,1,decimals,1,tooltip,sharpen transformed image") << QStringLiteral("tripod,type,position,value,0,min,0,max,100000,tooltip,reference frame")); + } else if (m_filtername == QLatin1String("videostab")) { fillParameters(QStringList(QStringLiteral("shutterangle,type,int,value,0,min,0,max,180,tooltip,Angle that Images could be maximum rotated"))); } connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &ClipStabilize::slotValidate); vbox = new QVBoxLayout(optionsbox); QHashIterator> hi(m_ui_params); m_tc.setFormat(KdenliveSettings::project_fps()); while (hi.hasNext()) { hi.next(); QHash val = hi.value(); if (val[QStringLiteral("type")] == QLatin1String("int") || val[QStringLiteral("type")] == QLatin1String("double")) { DoubleWidget *dbl = new DoubleWidget(hi.key() /*name*/, val[QStringLiteral("value")].toDouble(), val[QStringLiteral("min")].toDouble(), val[QStringLiteral("max")].toDouble(), val[QStringLiteral("value")].toDouble(), /*default*/ QString(), /*comment*/ 0 /*id*/, QString(), /*suffix*/ val[QStringLiteral("decimals")] != QString() ? val[QStringLiteral("decimals")].toInt() : 0, this); dbl->setObjectName(hi.key()); dbl->setToolTip(val[QStringLiteral("tooltip")]); connect(dbl, &DoubleWidget::valueChanged, this, &ClipStabilize::slotUpdateParams); vbox->addWidget(dbl); } else if (val[QStringLiteral("type")] == QLatin1String("bool")) { auto *ch = new QCheckBox(hi.key(), this); ch->setCheckState(val[QStringLiteral("value")] == QLatin1String("0") ? Qt::Unchecked : Qt::Checked); ch->setObjectName(hi.key()); connect(ch, &QCheckBox::stateChanged, this, &ClipStabilize::slotUpdateParams); ch->setToolTip(val[QStringLiteral("tooltip")]); vbox->addWidget(ch); } else if (val[QStringLiteral("type")] == QLatin1String("position")) { PositionWidget *posedit = new PositionWidget(hi.key(), 0, 0, out, m_tc, QString(), this); posedit->setToolTip(val[QStringLiteral("tooltip")]); posedit->setObjectName(hi.key()); vbox->addWidget(posedit); connect(posedit, &PositionWidget::valueChanged, this, &ClipStabilize::slotUpdateParams); } } adjustSize(); } ClipStabilize::~ClipStabilize() { /*if (m_stabilizeProcess.state() != QProcess::NotRunning) { m_stabilizeProcess.close(); }*/ KdenliveSettings::setAdd_new_clip(auto_add->isChecked()); } -QMap ClipStabilize::producerParams() const +std::unordered_map ClipStabilize::filterParams() const { - return QMap(); -} + std::unordered_map params; -QMap ClipStabilize::filterParams() const -{ - QMap params; - params.insert(QStringLiteral("filter"), m_filtername); - - QMapIterator i(m_fixedParams); - while (i.hasNext()) { - i.next(); - params.insert(i.key(), i.value()); + for (const auto &it : m_fixedParams) { + params[it.first] = it.second; } QHashIterator> it(m_ui_params); while (it.hasNext()) { it.next(); - params.insert(it.key(), it.value().value(QStringLiteral("value"))); + params[it.key()] = it.value().value(QStringLiteral("value")); } return params; } -QMap ClipStabilize::consumerParams() const +QString ClipStabilize::filterName() const { - // consumer params - QMap params; - params.insert(QStringLiteral("consumer"), QStringLiteral("xml")); - params.insert(QStringLiteral("all"), QStringLiteral("1")); - params.insert(QStringLiteral("title"), i18n("Stabilised")); - return params; + return m_filtername; } QString ClipStabilize::destination() const { QString path = dest_url->url().toLocalFile(); - if (m_urls.count() > 1 && !path.endsWith(QDir::separator())) { + if (m_binIds.size() > 1 && !path.endsWith(QDir::separator())) { path.append(QDir::separator()); } return path; } QString ClipStabilize::desc() const { return i18n("Stabilize clip"); } void ClipStabilize::slotUpdateParams() { for (int i = 0; i < vbox->count(); ++i) { QWidget *w = vbox->itemAt(i)->widget(); QString name = w->objectName(); if (!name.isEmpty() && m_ui_params.contains(name)) { if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("int") || m_ui_params[name][QStringLiteral("type")] == QLatin1String("double")) { DoubleWidget *dbl = static_cast(w); m_ui_params[name][QStringLiteral("value")] = QString::number((double)(dbl->getValue())); } else if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("bool")) { QCheckBox *ch = (QCheckBox *)w; m_ui_params[name][QStringLiteral("value")] = ch->checkState() == Qt::Checked ? QStringLiteral("1") : QStringLiteral("0"); } else if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("position")) { PositionWidget *pos = (PositionWidget *)w; m_ui_params[name][QStringLiteral("value")] = QString::number(pos->getPosition()); } } } } bool ClipStabilize::autoAddClip() const { return auto_add->isChecked(); } void ClipStabilize::fillParameters(QStringList lst) { m_ui_params.clear(); while (!lst.isEmpty()) { QString vallist = lst.takeFirst(); QStringList cont = vallist.split(QLatin1Char(',')); QString name = cont.takeFirst(); while (!cont.isEmpty()) { QString valname = cont.takeFirst(); QString val; if (!cont.isEmpty()) { val = cont.takeFirst(); } m_ui_params[name][valname] = val; } } } void ClipStabilize::slotValidate() { - if (m_urls.count() == 1) { + if (m_binIds.size() == 1) { if (QFile::exists(dest_url->url().toLocalFile())) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", dest_url->url().toLocalFile())) == KMessageBox::No) { return; } } } else { QDir folder(dest_url->url().toLocalFile()); QStringList existingFiles; - for (const QString &path : m_urls) { - if (folder.exists(path + QStringLiteral(".mlt"))) { - existingFiles.append(folder.absoluteFilePath(path + QStringLiteral(".mlt"))); + for (const QString &binId : m_binIds) { + auto binClip = pCore->projectItemModel()->getClipByBinID(binId); + auto url = binClip->url(); + if (folder.exists(url + QStringLiteral(".mlt"))) { + existingFiles.append(folder.absoluteFilePath(url + QStringLiteral(".mlt"))); } } if (!existingFiles.isEmpty()) { if (KMessageBox::warningContinueCancelList(this, i18n("The stabilize job will overwrite the following files:"), existingFiles) == KMessageBox::Cancel) { return; } } } accept(); } diff --git a/src/project/clipstabilize.h b/src/project/clipstabilize.h index bc3ee6788..3f88465de 100644 --- a/src/project/clipstabilize.h +++ b/src/project/clipstabilize.h @@ -1,65 +1,65 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2011 by Marco Gittler (marco@gitma.de) * * * * 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 CLIPSTABILIZE_H #define CLIPSTABILIZE_H +#include "definitions.h" #include "timecode.h" #include "ui_clipstabilize_ui.h" #include +#include class ClipStabilize : public QDialog, public Ui::ClipStabilize_UI { Q_OBJECT public: - explicit ClipStabilize(const QStringList &urls, const QString &filterName, int out, QWidget *parent = nullptr); + explicit ClipStabilize(const std::vector &binIds, const QString &filterName, int out, QWidget *parent = nullptr); ~ClipStabilize(); /** @brief Should the generated clip be added to current project. */ bool autoAddClip() const; - /** @brief Return the producer parameters, producer name as value of "producer" entry. */ - QMap producerParams() const; /** @brief Return the filter parameters, filter name as value of "filter" entry. */ - QMap filterParams() const; - /** @brief Return the consumer parameters, consumer name as value of "consumer" entry. */ - QMap consumerParams() const; + std::unordered_map filterParams() const; /** @brief Return the destination file or folder. */ QString destination() const; /** @brief Return the job description. */ QString desc() const; + /* Return the name of the actual mlt filter used */ + QString filterName() const; private slots: void slotUpdateParams(); void slotValidate(); private: QString m_filtername; - QStringList m_urls; + std::vector m_binIds; QHash> m_ui_params; QVBoxLayout *vbox; void fillParameters(QStringList); - QMap m_fixedParams; + std::unordered_map m_fixedParams; Timecode m_tc; signals: void addClip(const QUrl &url); }; #endif diff --git a/src/project/dialogs/archivewidget.cpp b/src/project/dialogs/archivewidget.cpp index c3e7185ec..ae2832f3f 100644 --- a/src/project/dialogs/archivewidget.cpp +++ b/src/project/dialogs/archivewidget.cpp @@ -1,1097 +1,1097 @@ /*************************************************************************** * 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 "archivewidget.h" #include "mltcontroller/clipcontroller.h" #include "projectsettings.h" #include "titler/titlewidget.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ArchiveWidget::ArchiveWidget(const QString &projectName, const QDomDocument &doc, const QList> &list, const QStringList &luma_list, QWidget *parent) : QDialog(parent) , m_requestedSize(0) , m_copyJob(nullptr) , m_name(projectName.section(QLatin1Char('.'), 0, -2)) , m_doc(doc) , m_temp(nullptr) , m_abortArchive(false) , m_extractMode(false) , m_progressTimer(nullptr) , m_extractArchive(nullptr) , m_missingClips(0) { setAttribute(Qt::WA_DeleteOnClose); setupUi(this); setWindowTitle(i18n("Archive Project")); archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath())); connect(archive_url, &KUrlRequester::textChanged, this, &ArchiveWidget::slotCheckSpace); connect(this, SIGNAL(archivingFinished(bool)), this, SLOT(slotArchivingFinished(bool))); connect(this, SIGNAL(archiveProgress(int)), this, SLOT(slotArchivingProgress(int))); connect(proxy_only, &QCheckBox::stateChanged, this, &ArchiveWidget::slotProxyOnly); // Setup categories QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips")); videos->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic"))); videos->setData(0, Qt::UserRole, QStringLiteral("videos")); videos->setExpanded(false); QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips")); sounds->setIcon(0, QIcon::fromTheme(QStringLiteral("audio-x-generic"))); sounds->setData(0, Qt::UserRole, QStringLiteral("sounds")); sounds->setExpanded(false); QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips")); images->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); images->setData(0, Qt::UserRole, QStringLiteral("images")); images->setExpanded(false); QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips")); slideshows->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); slideshows->setData(0, Qt::UserRole, QStringLiteral("slideshows")); slideshows->setExpanded(false); QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips")); texts->setIcon(0, QIcon::fromTheme(QStringLiteral("text-plain"))); texts->setData(0, Qt::UserRole, QStringLiteral("texts")); texts->setExpanded(false); QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist clips")); playlists->setIcon(0, QIcon::fromTheme(QStringLiteral("video-mlt-playlist"))); playlists->setData(0, Qt::UserRole, QStringLiteral("playlist")); playlists->setExpanded(false); QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips")); others->setIcon(0, QIcon::fromTheme(QStringLiteral("unknown"))); others->setData(0, Qt::UserRole, QStringLiteral("others")); others->setExpanded(false); QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files")); lumas->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); lumas->setData(0, Qt::UserRole, QStringLiteral("lumas")); lumas->setExpanded(false); QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips")); proxies->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic"))); proxies->setData(0, Qt::UserRole, QStringLiteral("proxy")); proxies->setExpanded(false); // process all files QStringList allFonts; QStringList extraImageUrls; QStringList otherUrls; generateItems(lumas, luma_list); QMap slideUrls; QMap audioUrls; QMap videoUrls; QMap imageUrls; QMap playlistUrls; QMap proxyUrls; for (int i = 0; i < list.count(); ++i) { const std::shared_ptr &clip = list.at(i); ClipType t = clip->clipType(); QString id = clip->binId(); - if (t == Color) { + if (t == ClipType::Color) { continue; } - if (t == SlideShow) { + if (t == ClipType::SlideShow) { // TODO: Slideshow files slideUrls.insert(id, clip->clipUrl()); - } else if (t == Image) { + } else if (t == ClipType::Image) { imageUrls.insert(id, clip->clipUrl()); - } else if (t == QText) { + } else if (t == ClipType::QText) { allFonts << clip->getProducerProperty(QStringLiteral("family")); - } else if (t == Text) { + } else if (t == ClipType::Text) { QStringList imagefiles = TitleWidget::extractImageList(clip->getProducerProperty(QStringLiteral("xmldata"))); QStringList fonts = TitleWidget::extractFontList(clip->getProducerProperty(QStringLiteral("xmldata"))); extraImageUrls << imagefiles; allFonts << fonts; - } else if (t == Playlist) { + } else if (t == ClipType::Playlist) { playlistUrls.insert(id, clip->clipUrl()); QStringList files = ProjectSettings::extractPlaylistUrls(clip->clipUrl()); otherUrls << files; } else if (!clip->clipUrl().isEmpty()) { - if (t == Audio) { + if (t == ClipType::Audio) { audioUrls.insert(id, clip->clipUrl()); } else { videoUrls.insert(id, clip->clipUrl()); // Check if we have a proxy QString proxy = clip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (!proxy.isEmpty() && proxy != QLatin1String("-") && QFile::exists(proxy)) { proxyUrls.insert(id, proxy); } } } } generateItems(images, extraImageUrls); generateItems(sounds, audioUrls); generateItems(videos, videoUrls); generateItems(images, imageUrls); generateItems(slideshows, slideUrls); generateItems(playlists, playlistUrls); generateItems(others, otherUrls); generateItems(proxies, proxyUrls); allFonts.removeDuplicates(); m_infoMessage = new KMessageWidget(this); QVBoxLayout *s = static_cast(layout()); s->insertWidget(5, m_infoMessage); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->setWordWrap(true); m_infoMessage->hide(); // missing clips, warn user if (m_missingClips > 0) { QString infoText = i18np("You have %1 missing clip in your project.", "You have %1 missing clips in your project.", m_missingClips); m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(infoText); m_infoMessage->animatedShow(); } // TODO: fonts // Hide unused categories, add item count int total = 0; for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); if (items == 0) { files_list->topLevelItem(i)->setHidden(true); } else { if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) { // Special case: slideshows contain several files for (int j = 0; j < items; ++j) { total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count(); } } else { total += items; } parentItem->setText(0, files_list->topLevelItem(i)->text(0) + QLatin1Char(' ') + i18np("(%1 item)", "(%1 items)", items)); } } if (m_name.isEmpty()) { m_name = i18n("Untitled"); } compressed_archive->setText(compressed_archive->text() + QStringLiteral(" (") + m_name + QStringLiteral(".tar.gz)")); project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize))); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartArchiving); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); slotCheckSpace(); } // Constructor for extract widget ArchiveWidget::ArchiveWidget(const QUrl &url, QWidget *parent) : QDialog(parent) , m_requestedSize(0) , m_copyJob(nullptr) , m_temp(nullptr) , m_abortArchive(false) , m_extractMode(true) , m_extractUrl(url) , m_extractArchive(nullptr) , m_missingClips(0) , m_infoMessage(nullptr) { // setAttribute(Qt::WA_DeleteOnClose); setupUi(this); m_progressTimer = new QTimer; m_progressTimer->setInterval(800); m_progressTimer->setSingleShot(false); connect(m_progressTimer, &QTimer::timeout, this, &ArchiveWidget::slotExtractProgress); connect(this, &ArchiveWidget::extractingFinished, this, &ArchiveWidget::slotExtractingFinished); connect(this, &ArchiveWidget::showMessage, this, &ArchiveWidget::slotDisplayMessage); compressed_archive->setHidden(true); proxy_only->setHidden(true); project_files->setHidden(true); files_list->setHidden(true); label->setText(i18n("Extract to")); setWindowTitle(i18n("Open Archived Project")); archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath())); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Extract")); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartExtracting); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); adjustSize(); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::openArchiveForExtraction); } ArchiveWidget::~ArchiveWidget() { delete m_extractArchive; delete m_progressTimer; } void ArchiveWidget::slotDisplayMessage(const QString &icon, const QString &text) { icon_info->setPixmap(QIcon::fromTheme(icon).pixmap(16, 16)); text_info->setText(text); } void ArchiveWidget::slotJobResult(bool success, const QString &text) { m_infoMessage->setMessageType(success ? KMessageWidget::Positive : KMessageWidget::Warning); m_infoMessage->setText(text); m_infoMessage->animatedShow(); } void ArchiveWidget::openArchiveForExtraction() { emit showMessage(QStringLiteral("system-run"), i18n("Opening archive...")); m_extractArchive = new KTar(m_extractUrl.toLocalFile()); if (!m_extractArchive->isOpen() && !m_extractArchive->open(QIODevice::ReadOnly)) { emit showMessage(QStringLiteral("dialog-close"), i18n("Cannot open archive file:\n %1", m_extractUrl.toLocalFile())); groupBox->setEnabled(false); return; } // Check that it is a kdenlive project archive bool isProjectArchive = false; QStringList files = m_extractArchive->directory()->entries(); for (int i = 0; i < files.count(); ++i) { if (files.at(i).endsWith(QLatin1String(".kdenlive"))) { m_projectName = files.at(i); isProjectArchive = true; break; } } if (!isProjectArchive) { emit showMessage(QStringLiteral("dialog-close"), i18n("File %1\n is not an archived Kdenlive project", m_extractUrl.toLocalFile())); groupBox->setEnabled(false); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); return; } buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); emit showMessage(QStringLiteral("dialog-ok"), i18n("Ready")); } void ArchiveWidget::done(int r) { if (closeAccepted()) { QDialog::done(r); } } void ArchiveWidget::closeEvent(QCloseEvent *e) { if (closeAccepted()) { e->accept(); } else { e->ignore(); } } bool ArchiveWidget::closeAccepted() { if (!m_extractMode && !archive_url->isEnabled()) { // Archiving in progress, should we stop? if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) { return false; } if (m_copyJob) { m_copyJob->kill(); } } return true; } void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QStringList &items) { QStringList filesList; QString fileName; int ix = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (const QString &file : items) { QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file); fileName = QUrl::fromLocalFile(file).fileName(); if (isSlideshow) { // we store each slideshow in a separate subdirectory item->setData(0, Qt::UserRole, ix); ix++; QUrl slideUrl = QUrl::fromLocalFile(file); QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile()); if (slideUrl.fileName().startsWith(QLatin1String(".all."))) { // MIME type slideshow (for example *.png) QStringList filters; // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1); dir.setNameFilters(filters); QFileInfoList resultList = dir.entryInfoList(QDir::Files); QStringList slideImages; qint64 totalSize = 0; for (int i = 0; i < resultList.count(); ++i) { totalSize += resultList.at(i).size(); slideImages << resultList.at(i).absoluteFilePath(); } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } else { // pattern url (like clip%.3d.png) QStringList result = dir.entryList(QDir::Files); QString filter = slideUrl.fileName(); QString ext = filter.section(QLatin1Char('.'), -1); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); QStringList slideImages; QString directory = dir.absolutePath(); if (!directory.endsWith(QLatin1Char('/'))) { directory.append(QLatin1Char('/')); } qint64 totalSize = 0; for (const QString &path : result) { if (rx.exactMatch(path)) { totalSize += QFileInfo(directory + path).size(); slideImages << directory + path; } } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } } else if (filesList.contains(fileName)) { // we have 2 files with same name int i = 0; QString newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); while (filesList.contains(newFileName)) { i++; newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); } fileName = newFileName; item->setData(0, Qt::UserRole, fileName); } if (!isSlideshow) { qint64 fileSize = QFileInfo(file).size(); if (fileSize <= 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete"))); m_missingClips++; } else { m_requestedSize += static_cast(fileSize); item->setData(0, Qt::UserRole + 3, fileSize); } filesList << fileName; } } } void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QMap &items) { QStringList filesList; QString fileName; int ix = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); QMap::const_iterator it = items.constBegin(); while (it != items.constEnd()) { QString file = it.value(); QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file); // Store the clip's id item->setData(0, Qt::UserRole + 2, it.key()); fileName = QUrl::fromLocalFile(file).fileName(); if (isSlideshow) { // we store each slideshow in a separate subdirectory item->setData(0, Qt::UserRole, ix); ix++; QUrl slideUrl = QUrl::fromLocalFile(file); QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile()); if (slideUrl.fileName().startsWith(QLatin1String(".all."))) { // MIME type slideshow (for example *.png) QStringList filters; // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1); dir.setNameFilters(filters); QFileInfoList resultList = dir.entryInfoList(QDir::Files); QStringList slideImages; qint64 totalSize = 0; for (int i = 0; i < resultList.count(); ++i) { totalSize += resultList.at(i).size(); slideImages << resultList.at(i).absoluteFilePath(); } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } else { // pattern url (like clip%.3d.png) QStringList result = dir.entryList(QDir::Files); QString filter = slideUrl.fileName(); QString ext = filter.section(QLatin1Char('.'), -1).section(QLatin1Char('?'), 0, 0); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); QStringList slideImages; qint64 totalSize = 0; for (const QString &path : result) { if (rx.exactMatch(path)) { totalSize += QFileInfo(dir.absoluteFilePath(path)).size(); slideImages << dir.absoluteFilePath(path); } } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } } else if (filesList.contains(fileName)) { // we have 2 files with same name int index2 = 0; QString newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(index2) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); while (filesList.contains(newFileName)) { index2++; newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(index2) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); } fileName = newFileName; item->setData(0, Qt::UserRole, fileName); } if (!isSlideshow) { qint64 fileSize = QFileInfo(file).size(); if (fileSize <= 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete"))); m_missingClips++; } else { m_requestedSize += static_cast(fileSize); item->setData(0, Qt::UserRole + 3, fileSize); } filesList << fileName; } ++it; } } void ArchiveWidget::slotCheckSpace() { KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo(archive_url->url().toLocalFile()); KIO::filesize_t freeSize = inf.available(); if (freeSize > m_requestedSize) { // everything is ok buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); slotDisplayMessage(QStringLiteral("dialog-ok"), i18n("Available space on drive: %1", KIO::convertSize(freeSize))); } else { buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); slotDisplayMessage(QStringLiteral("dialog-close"), i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize))); } } bool ArchiveWidget::slotStartArchiving(bool firstPass) { if (firstPass && ((m_copyJob != nullptr) || m_archiveThread.isRunning())) { // archiving in progress, abort if (m_copyJob) { m_copyJob->kill(KJob::EmitResult); } m_abortArchive = true; return true; } bool isArchive = compressed_archive->isChecked(); if (!firstPass) { m_copyJob = nullptr; } else { // starting archiving m_abortArchive = false; m_duplicateFiles.clear(); m_replacementList.clear(); m_foldersList.clear(); m_filesList.clear(); slotDisplayMessage(QStringLiteral("system-run"), i18n("Archiving...")); repaint(); archive_url->setEnabled(false); proxy_only->setEnabled(false); compressed_archive->setEnabled(false); } QList files; QUrl destUrl; QString destPath; QTreeWidgetItem *parentItem; bool isSlideshow = false; int items = 0; // We parse all files going into one folder, then start the copy job for (int i = 0; i < files_list->topLevelItemCount(); ++i) { parentItem = files_list->topLevelItem(i); if (parentItem->isDisabled()) { parentItem->setExpanded(false); continue; } if (parentItem->childCount() > 0) { if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) { QUrl slideFolder = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QStringLiteral("/slideshows")); if (isArchive) { m_foldersList.append(QStringLiteral("slideshows")); } else { QDir dir(slideFolder.toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", slideFolder.toLocalFile())); } } isSlideshow = true; } else { isSlideshow = false; } files_list->setCurrentItem(parentItem); parentItem->setExpanded(true); destPath = parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/'); destUrl = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QLatin1Char('/') + destPath); QTreeWidgetItem *item; for (int j = 0; j < parentItem->childCount(); ++j) { item = parentItem->child(j); if (item->isDisabled()) { continue; } // Special case: slideshows items++; if (isSlideshow) { destPath += item->data(0, Qt::UserRole).toString() + QLatin1Char('/'); destUrl = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QDir::separator() + destPath); QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList(); for (int k = 0; k < srcFiles.count(); ++k) { files << QUrl::fromLocalFile(srcFiles.at(k)); } item->setDisabled(true); if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) { // We have processed all slideshows parentItem->setDisabled(true); } break; } else if (item->data(0, Qt::UserRole).isNull()) { files << QUrl::fromLocalFile(item->text(0)); } else { // We must rename the destination file, since another file with same name exists // TODO: monitor progress if (isArchive) { m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString()); } else { m_duplicateFiles.insert(QUrl::fromLocalFile(item->text(0)), QUrl::fromLocalFile(destUrl.toLocalFile() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString())); } } } if (!isSlideshow) { parentItem->setDisabled(true); } break; } } if (items == 0) { // No clips to archive slotArchivingFinished(nullptr, true); return true; } if (destPath.isEmpty()) { if (m_duplicateFiles.isEmpty()) { return false; } QMapIterator i(m_duplicateFiles); if (i.hasNext()) { i.next(); QUrl startJobSrc = i.key(); QUrl startJobDst = i.value(); m_duplicateFiles.remove(startJobSrc); KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo); connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *))); connect(job, SIGNAL(processedSize(KJob *, KIO::filesize_t)), this, SLOT(slotArchivingProgress(KJob *, KIO::filesize_t))); } return true; } if (isArchive) { m_foldersList.append(destPath); for (int i = 0; i < files.count(); ++i) { m_filesList.insert(files.at(i).toLocalFile(), destPath + files.at(i).fileName()); } slotArchivingFinished(); } else if (files.isEmpty()) { slotStartArchiving(false); } else { QDir dir(destUrl.toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", destUrl.toLocalFile())); } m_copyJob = KIO::copy(files, destUrl, KIO::HideProgressInfo); connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *))); connect(m_copyJob, SIGNAL(processedSize(KJob *, KIO::filesize_t)), this, SLOT(slotArchivingProgress(KJob *, KIO::filesize_t))); } if (firstPass) { progressBar->setValue(0); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort")); } return true; } void ArchiveWidget::slotArchivingFinished(KJob *job, bool finished) { if (job == nullptr || job->error() == 0) { if (!finished && slotStartArchiving(false)) { // We still have files to archive return; } if (!compressed_archive->isChecked()) { // Archiving finished progressBar->setValue(100); if (processProjectFile()) { slotJobResult(true, i18n("Project was successfully archived.")); } else { slotJobResult(false, i18n("There was an error processing project file")); } } else { processProjectFile(); } } else { m_copyJob = nullptr; slotJobResult(false, i18n("There was an error while copying the files: %1", job->errorString())); } if (!compressed_archive->isChecked()) { buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); archive_url->setEnabled(true); proxy_only->setEnabled(true); compressed_archive->setEnabled(true); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { files_list->topLevelItem(i)->setDisabled(false); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { files_list->topLevelItem(i)->child(j)->setDisabled(false); } } } } void ArchiveWidget::slotArchivingProgress(KJob *, KIO::filesize_t size) { progressBar->setValue(static_cast(100 * size / m_requestedSize)); } bool ArchiveWidget::processProjectFile() { QTreeWidgetItem *item; bool isArchive = compressed_archive->isChecked(); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); if (parentItem->childCount() > 0) { QDir destFolder(archive_url->url().toLocalFile() + QDir::separator() + parentItem->data(0, Qt::UserRole).toString()); bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (int j = 0; j < parentItem->childCount(); ++j) { item = parentItem->child(j); QUrl src = QUrl::fromLocalFile(item->text(0)); QUrl dest = QUrl::fromLocalFile(destFolder.absolutePath()); if (isSlideshow) { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName()); } else if (item->data(0, Qt::UserRole).isNull()) { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName()); } else { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString()); } m_replacementList.insert(src, dest); } } } QDomElement mlt = m_doc.documentElement(); QString root = mlt.attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } // Adjust global settings QString basePath; if (isArchive) { basePath = QStringLiteral("$CURRENTPATH"); } else { basePath = archive_url->url().adjusted(QUrl::StripTrailingSlash | QUrl::StripTrailingSlash).toLocalFile(); } // Switch to relative path mlt.removeAttribute(QStringLiteral("root")); // process kdenlive producers QDomNodeList prods = mlt.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } if (e.hasAttribute(QStringLiteral("resource"))) { QUrl src = QUrl::fromLocalFile(e.attribute(QStringLiteral("resource"))); QUrl dest = m_replacementList.value(src); if (!dest.isEmpty()) { e.setAttribute(QStringLiteral("resource"), dest.toLocalFile()); } } if (e.hasAttribute(QStringLiteral("kdenlive:proxy")) && e.attribute(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { QUrl src = QUrl::fromLocalFile(e.attribute(QStringLiteral("kdenlive:proxy"))); QUrl dest = m_replacementList.value(src); if (!dest.isEmpty()) { e.setAttribute(QStringLiteral("kdenlive:proxy"), dest.toLocalFile()); } } } // process mlt producers prods = mlt.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } QString src = EffectsList::property(e, QStringLiteral("resource")); if (!src.isEmpty()) { if (QFileInfo(src).isRelative()) { src.prepend(root); } QUrl srcUrl = QUrl::fromLocalFile(src); QUrl dest = m_replacementList.value(srcUrl); if (!dest.isEmpty()) { EffectsList::setProperty(e, QStringLiteral("resource"), dest.toLocalFile()); } } src = EffectsList::property(e, QStringLiteral("xmldata")); bool found = false; if (!src.isEmpty() && (src.contains(QLatin1String("QGraphicsPixmapItem")) || src.contains(QLatin1String("QGraphicsSvgItem")))) { // Title with images, replace paths QDomDocument titleXML; titleXML.setContent(src); QDomNodeList images = titleXML.documentElement().elementsByTagName(QLatin1String("item")); for (int j = 0; j < images.count(); ++j) { QDomNode n = images.at(j); QDomElement url = n.firstChildElement(QLatin1String("content")); if (!url.isNull() && url.hasAttribute(QLatin1String("url"))) { QUrl srcUrl = QUrl::fromLocalFile(url.attribute(QLatin1String("url"))); QUrl dest = m_replacementList.value(srcUrl); if (dest.isValid()) { url.setAttribute(QLatin1String("url"), dest.toLocalFile()); found = true; } } } if (found) { // replace content EffectsList::setProperty(e, QStringLiteral("xmldata"), titleXML.toString()); } } } // process mlt transitions (for luma files) prods = mlt.elementsByTagName(QStringLiteral("transition")); QString attribute; for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } attribute = QStringLiteral("resource"); QString src = EffectsList::property(e, attribute); if (src.isEmpty()) { attribute = QStringLiteral("luma"); } src = EffectsList::property(e, attribute); if (!src.isEmpty()) { if (QFileInfo(src).isRelative()) { src.prepend(root); } QUrl srcUrl = QUrl::fromLocalFile(src); QUrl dest = m_replacementList.value(srcUrl); if (!dest.isEmpty()) { EffectsList::setProperty(e, attribute, dest.toLocalFile()); } } } QString playList = m_doc.toString(); if (isArchive) { QString startString(QStringLiteral("\"")); startString.append(archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile()); QString endString(QStringLiteral("\"")); endString.append(basePath); playList.replace(startString, endString); startString = QLatin1Char('>') + archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile(); endString = QLatin1Char('>') + basePath; playList.replace(startString, endString); } if (isArchive) { m_temp = new QTemporaryFile; if (!m_temp->open()) { KMessageBox::error(this, i18n("Cannot create temporary file")); } m_temp->write(playList.toUtf8()); m_temp->close(); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive); return true; } QString path = archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral(".kdenlive"); QFile file(path); if (file.exists() && KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return false; } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << path; KMessageBox::error(this, i18n("Cannot write to file %1", path)); return false; } file.write(m_doc.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", path)); file.close(); return false; } file.close(); return true; } void ArchiveWidget::createArchive() { QString archiveName(archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral(".tar.gz")); if (QFile::exists(archiveName) && KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", archiveName)) == KMessageBox::No) { return; } QFileInfo dirInfo(archive_url->url().toLocalFile()); QString user = dirInfo.owner(); QString group = dirInfo.group(); KTar archive(archiveName, QStringLiteral("application/x-gzip")); archive.open(QIODevice::WriteOnly); // Create folders for (const QString &path : m_foldersList) { archive.writeDir(path, user, group); } // Add files int ix = 0; QMapIterator i(m_filesList); while (i.hasNext()) { i.next(); archive.addLocalFile(i.key(), i.value()); emit archiveProgress((int)100 * ix / m_filesList.count()); ix++; } // Add project file bool result = false; if (m_temp) { archive.addLocalFile(m_temp->fileName(), m_name + QStringLiteral(".kdenlive")); result = archive.close(); delete m_temp; m_temp = nullptr; } emit archivingFinished(result); } void ArchiveWidget::slotArchivingFinished(bool result) { if (result) { slotJobResult(true, i18n("Project was successfully archived.")); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } else { slotJobResult(false, i18n("There was an error processing project file")); } progressBar->setValue(100); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); archive_url->setEnabled(true); proxy_only->setEnabled(true); compressed_archive->setEnabled(true); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { files_list->topLevelItem(i)->setDisabled(false); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { files_list->topLevelItem(i)->child(j)->setDisabled(false); } } } void ArchiveWidget::slotArchivingProgress(int p) { progressBar->setValue(p); } void ArchiveWidget::slotStartExtracting() { if (m_archiveThread.isRunning()) { // TODO: abort extracting return; } QFileInfo f(m_extractUrl.toLocalFile()); m_requestedSize = static_cast(f.size()); QDir dir(archive_url->url().toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", archive_url->url().toLocalFile())); } slotDisplayMessage(QStringLiteral("system-run"), i18n("Extracting...")); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort")); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::doExtracting); m_progressTimer->start(); } void ArchiveWidget::slotExtractProgress() { KIO::DirectorySizeJob *job = KIO::directorySize(archive_url->url()); connect(job, &KJob::result, this, &ArchiveWidget::slotGotProgress); } void ArchiveWidget::slotGotProgress(KJob *job) { if (!job->error()) { KIO::DirectorySizeJob *j = static_cast (job); progressBar->setValue(static_cast(100 * j->totalSize() / m_requestedSize)); } job->deleteLater(); } void ArchiveWidget::doExtracting() { m_extractArchive->directory()->copyTo(archive_url->url().toLocalFile() + QDir::separator()); m_extractArchive->close(); emit extractingFinished(); } QString ArchiveWidget::extractedProjectFile() const { return archive_url->url().toLocalFile() + QDir::separator() + m_projectName; } void ArchiveWidget::slotExtractingFinished() { m_progressTimer->stop(); // Process project file QFile file(extractedProjectFile()); bool error = false; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { error = true; } else { QString playList = QString::fromUtf8(file.readAll()); file.close(); if (playList.isEmpty()) { error = true; } else { playList.replace(QLatin1String("$CURRENTPATH"), archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile()); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: "; error = true; } else { file.write(playList.toUtf8()); if (file.error() != QFile::NoError) { error = true; } file.close(); } } } if (error) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Cannot open project file %1", extractedProjectFile()), i18n("Cannot open file")); reject(); } else { accept(); } } void ArchiveWidget::slotProxyOnly(int onlyProxy) { m_requestedSize = 0; if (onlyProxy == Qt::Checked) { // Archive proxy clips QStringList proxyIdList; QTreeWidgetItem *parentItem = nullptr; // Build list of existing proxy ids for (int i = 0; i < files_list->topLevelItemCount(); ++i) { parentItem = files_list->topLevelItem(i); if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) { break; } } if (!parentItem) { return; } int items = parentItem->childCount(); for (int j = 0; j < items; ++j) { proxyIdList << parentItem->child(j)->data(0, Qt::UserRole + 2).toString(); } // Parse all items to disable original clips for existing proxies for (int i = 0; i < proxyIdList.count(); ++i) { const QString &id = proxyIdList.at(i); if (id.isEmpty()) { continue; } for (int j = 0; j < files_list->topLevelItemCount(); ++j) { parentItem = files_list->topLevelItem(j); if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) { continue; } items = parentItem->childCount(); for (int k = 0; k < items; ++k) { if (parentItem->child(k)->data(0, Qt::UserRole + 2).toString() == id) { // This item has a proxy, do not archive it parentItem->child(k)->setFlags(Qt::ItemIsSelectable); break; } } } } } else { // Archive all clips for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); for (int j = 0; j < items; ++j) { parentItem->child(j)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); } } } // Calculate requested size int total = 0; for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); int itemsCount = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (int j = 0; j < items; ++j) { if (!parentItem->child(j)->isDisabled()) { m_requestedSize += static_cast(parentItem->child(j)->data(0, Qt::UserRole + 3).toInt()); if (isSlideshow) { total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count(); } else { total++; } itemsCount++; } } parentItem->setText(0, parentItem->text(0).section(QLatin1Char('('), 0, 0) + i18np("(%1 item)", "(%1 items)", itemsCount)); } project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize))); slotCheckSpace(); } diff --git a/src/project/dialogs/projectsettings.cpp b/src/project/dialogs/projectsettings.cpp index 858a60fb7..94b4e6761 100644 --- a/src/project/dialogs/projectsettings.cpp +++ b/src/project/dialogs/projectsettings.cpp @@ -1,782 +1,782 @@ /*************************************************************************** * Copyright (C) 2016 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 "projectsettings.h" #include "bin/bin.h" #include "core.h" #include "dialogs/encodingprofilesdialog.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "effectslist/effectslist.h" #include "kdenlivesettings.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/profilewidget.h" #include "project/dialogs/temporarydata.h" #include "titler/titlewidget.h" #include "utils/KoIconUtils.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include class NoEditDelegate : public QStyledItemDelegate { public: NoEditDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { Q_UNUSED(parent); Q_UNUSED(option); Q_UNUSED(index); return nullptr; } }; ProjectSettings::ProjectSettings(KdenliveDoc *doc, QMap metadata, const QStringList &lumas, int videotracks, int audiotracks, const QString & /*projectPath*/, bool readOnlyTracks, bool savedProject, QWidget *parent) : QDialog(parent) , m_savedProject(savedProject) , m_lumas(lumas) { setupUi(this); tabWidget->setTabBarAutoHide(true); auto *vbox = new QVBoxLayout; m_pw = new ProfileWidget(this); vbox->addWidget(m_pw); profile_box->setLayout(vbox); profile_box->setTitle(i18n("Select the profile (preset) of the project")); list_search->setTreeWidget(files_list); project_folder->setMode(KFile::Directory); m_buttonOk = buttonBox->button(QDialogButtonBox::Ok); // buttonOk->setEnabled(false); audio_thumbs->setChecked(KdenliveSettings::audiothumbnails()); video_thumbs->setChecked(KdenliveSettings::videothumbnails()); audio_tracks->setValue(audiotracks); video_tracks->setValue(videotracks); connect(generate_proxy, &QAbstractButton::toggled, proxy_minsize, &QWidget::setEnabled); connect(generate_imageproxy, &QAbstractButton::toggled, proxy_imageminsize, &QWidget::setEnabled); QString currentProf; if (doc) { currentProf = pCore->getCurrentProfile()->path(); enable_proxy->setChecked(doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0); generate_proxy->setChecked(doc->getDocumentProperty(QStringLiteral("generateproxy")).toInt() != 0); proxy_minsize->setValue(doc->getDocumentProperty(QStringLiteral("proxyminsize")).toInt()); m_proxyparameters = doc->getDocumentProperty(QStringLiteral("proxyparams")); generate_imageproxy->setChecked(doc->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() != 0); proxy_imageminsize->setValue(doc->getDocumentProperty(QStringLiteral("proxyimageminsize")).toInt()); m_proxyextension = doc->getDocumentProperty(QStringLiteral("proxyextension")); m_previewparams = doc->getDocumentProperty(QStringLiteral("previewparameters")); m_previewextension = doc->getDocumentProperty(QStringLiteral("previewextension")); QString storageFolder = doc->getDocumentProperty(QStringLiteral("storagefolder")); if (!storageFolder.isEmpty()) { custom_folder->setChecked(true); } project_folder->setUrl(QUrl::fromLocalFile(doc->projectTempFolder())); auto *cacheWidget = new TemporaryData(doc, true, this); connect(cacheWidget, &TemporaryData::disableProxies, this, &ProjectSettings::disableProxies); connect(cacheWidget, &TemporaryData::disablePreview, this, &ProjectSettings::disablePreview); tabWidget->addTab(cacheWidget, i18n("Cache Data")); } else { currentProf = KdenliveSettings::default_profile(); enable_proxy->setChecked(KdenliveSettings::enableproxy()); generate_proxy->setChecked(KdenliveSettings::generateproxy()); proxy_minsize->setValue(KdenliveSettings::proxyminsize()); m_proxyparameters = KdenliveSettings::proxyparams(); generate_imageproxy->setChecked(KdenliveSettings::generateimageproxy()); proxy_imageminsize->setValue(KdenliveSettings::proxyimageminsize()); m_proxyextension = KdenliveSettings::proxyextension(); m_previewparams = KdenliveSettings::previewparams(); m_previewextension = KdenliveSettings::previewextension(); custom_folder->setChecked(KdenliveSettings::customprojectfolder()); project_folder->setUrl(QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))); } // Select profile m_pw->loadProfile(currentProf); proxy_minsize->setEnabled(generate_proxy->isChecked()); proxy_imageminsize->setEnabled(generate_imageproxy->isChecked()); loadProxyProfiles(); loadPreviewProfiles(); // Proxy GUI stuff proxy_showprofileinfo->setIcon(KoIconUtils::themedIcon(QStringLiteral("help-about"))); proxy_showprofileinfo->setToolTip(i18n("Show default profile parameters")); proxy_manageprofile->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); proxy_manageprofile->setToolTip(i18n("Manage proxy profiles")); connect(proxy_manageprofile, &QAbstractButton::clicked, this, &ProjectSettings::slotManageEncodingProfile); proxy_profile->setToolTip(i18n("Select default proxy profile")); connect(proxy_profile, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateProxyParams())); proxyparams->setVisible(false); proxyparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); connect(proxy_showprofileinfo, &QAbstractButton::clicked, proxyparams, &QWidget::setVisible); // Preview GUI stuff preview_showprofileinfo->setIcon(KoIconUtils::themedIcon(QStringLiteral("help-about"))); preview_showprofileinfo->setToolTip(i18n("Show default profile parameters")); preview_manageprofile->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); preview_manageprofile->setToolTip(i18n("Manage timeline preview profiles")); connect(preview_manageprofile, &QAbstractButton::clicked, this, &ProjectSettings::slotManagePreviewProfile); preview_profile->setToolTip(i18n("Select default preview profile")); connect(preview_profile, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdatePreviewParams())); previewparams->setVisible(false); previewparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); connect(preview_showprofileinfo, &QAbstractButton::clicked, previewparams, &QWidget::setVisible); if (readOnlyTracks) { video_tracks->setEnabled(false); audio_tracks->setEnabled(false); } metadata_list->setItemDelegateForColumn(0, new NoEditDelegate(this)); connect(metadata_list, &QTreeWidget::itemDoubleClicked, this, &ProjectSettings::slotEditMetadata); // Metadata list QTreeWidgetItem *item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Title")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.title.markup")); if (metadata.contains(QStringLiteral("meta.attr.title.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.title.markup"))); metadata.remove(QStringLiteral("meta.attr.title.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Author")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.author.markup")); if (metadata.contains(QStringLiteral("meta.attr.author.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.author.markup"))); metadata.remove(QStringLiteral("meta.attr.author.markup")); } else if (metadata.contains(QStringLiteral("meta.attr.artist.markup"))) { item->setText(0, i18n("Artist")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.artist.markup")); item->setText(1, metadata.value(QStringLiteral("meta.attr.artist.markup"))); metadata.remove(QStringLiteral("meta.attr.artist.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Copyright")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.copyright.markup")); if (metadata.contains(QStringLiteral("meta.attr.copyright.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.copyright.markup"))); metadata.remove(QStringLiteral("meta.attr.copyright.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Year")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.year.markup")); if (metadata.contains(QStringLiteral("meta.attr.year.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.year.markup"))); metadata.remove(QStringLiteral("meta.attr.year.markup")); } else if (metadata.contains(QStringLiteral("meta.attr.date.markup"))) { item->setText(0, i18n("Date")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.date.markup")); item->setText(1, metadata.value(QStringLiteral("meta.attr.date.markup"))); metadata.remove(QStringLiteral("meta.attr.date.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); QMap::const_iterator meta = metadata.constBegin(); while (meta != metadata.constEnd()) { item = new QTreeWidgetItem(metadata_list, QStringList() << meta.key().section(QLatin1Char('.'), 2, 2)); item->setData(0, Qt::UserRole, meta.key()); item->setText(1, meta.value()); item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); ++meta; } connect(add_metadata, &QAbstractButton::clicked, this, &ProjectSettings::slotAddMetadataField); connect(delete_metadata, &QAbstractButton::clicked, this, &ProjectSettings::slotDeleteMetadataField); add_metadata->setIcon(KoIconUtils::themedIcon(QStringLiteral("list-add"))); delete_metadata->setIcon(KoIconUtils::themedIcon(QStringLiteral("list-remove"))); if (doc != nullptr) { slotUpdateFiles(); connect(delete_unused, &QAbstractButton::clicked, this, &ProjectSettings::slotDeleteUnused); } else { tabWidget->removeTab(2); tabWidget->removeTab(1); } connect(project_folder, &KUrlRequester::textChanged, this, &ProjectSettings::slotUpdateButton); connect(button_export, &QAbstractButton::clicked, this, &ProjectSettings::slotExportToText); // Delete unused files is not implemented delete_unused->setVisible(false); } void ProjectSettings::slotEditMetadata(QTreeWidgetItem *item, int) { metadata_list->editItem(item, 1); } void ProjectSettings::slotDeleteUnused() { QStringList toDelete; // TODO /* QList list = m_projectList->documentClipList(); for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip->numReferences() == 0 && clip->clipType() != SlideShow) { QUrl url = clip->fileURL(); if (url.isValid() && !toDelete.contains(url.path())) toDelete << url.path(); } } // make sure our urls are not used in another clip for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip->numReferences() > 0) { QUrl url = clip->fileURL(); if (url.isValid() && toDelete.contains(url.path())) toDelete.removeAll(url.path()); } } if (toDelete.count() == 0) { // No physical url to delete, we only remove unused clips from project (color clips for example have no physical url) if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) return; m_projectList->cleanup(); slotUpdateFiles(); return; } if (KMessageBox::warningYesNoList(this, i18n("This will remove the following files from your hard drive.\nThis action cannot be undone, only use if you know what you are doing.\nAre you sure you want to continue?"), toDelete, i18n("Delete unused clips")) != KMessageBox::Yes) return; m_projectList->trashUnusedClips(); slotUpdateFiles(); */ } void ProjectSettings::slotUpdateFiles(bool cacheOnly) { // Get list of current project hashes QStringList hashes = pCore->binController()->getProjectHashes(); m_projectProxies.clear(); m_projectThumbs.clear(); if (cacheOnly) { return; } QList> list = pCore->binController()->getControllerList(); files_list->clear(); // List all files that are used in the project. That also means: // images included in slideshow and titles, files in playlist clips // TODO: images used in luma transitions? // Setup categories QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips")); videos->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("video-x-generic"))); videos->setExpanded(true); QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips")); sounds->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("audio-x-generic"))); sounds->setExpanded(true); QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips")); images->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("image-x-generic"))); images->setExpanded(true); QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips")); slideshows->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("image-x-generic"))); slideshows->setExpanded(true); QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips")); texts->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("text-plain"))); texts->setExpanded(true); QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist clips")); playlists->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("video-mlt-playlist"))); playlists->setExpanded(true); QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips")); others->setIcon(0, KoIconUtils::themedIcon(QStringLiteral("unknown"))); others->setExpanded(true); int count = 0; QStringList allFonts; for (const QString &file : m_lumas) { count++; new QTreeWidgetItem(images, QStringList() << file); } for (int i = 0; i < list.count(); ++i) { const std::shared_ptr &clip = list.at(i); - if (clip->clipType() == Color) { + if (clip->clipType() == ClipType::Color) { // ignore color clips in list, there is no real file continue; } - if (clip->clipType() == SlideShow) { + if (clip->clipType() == ClipType::SlideShow) { const QStringList subfiles = extractSlideshowUrls(clip->clipUrl()); for (const QString &file : subfiles) { count++; new QTreeWidgetItem(slideshows, QStringList() << file); } continue; } else if (!clip->clipUrl().isEmpty()) { // allFiles.append(clip->fileURL().path()); switch (clip->clipType()) { - case Text: + case ClipType::Text: new QTreeWidgetItem(texts, QStringList() << clip->clipUrl()); break; - case Audio: + case ClipType::Audio: new QTreeWidgetItem(sounds, QStringList() << clip->clipUrl()); break; - case Image: + case ClipType::Image: new QTreeWidgetItem(images, QStringList() << clip->clipUrl()); break; - case Playlist: + case ClipType::Playlist: new QTreeWidgetItem(playlists, QStringList() << clip->clipUrl()); break; - case Unknown: + case ClipType::Unknown: new QTreeWidgetItem(others, QStringList() << clip->clipUrl()); break; default: new QTreeWidgetItem(videos, QStringList() << clip->clipUrl()); break; } count++; } - if (clip->clipType() == Text) { + if (clip->clipType() == ClipType::Text) { const QStringList imagefiles = TitleWidget::extractImageList(clip->getProducerProperty(QStringLiteral("xmldata"))); const QStringList fonts = TitleWidget::extractFontList(clip->getProducerProperty(QStringLiteral("xmldata"))); for (const QString &file : imagefiles) { count++; new QTreeWidgetItem(images, QStringList() << file); } allFonts << fonts; - } else if (clip->clipType() == Playlist) { + } else if (clip->clipType() == ClipType::Playlist) { const QStringList files = extractPlaylistUrls(clip->clipUrl()); for (const QString &file : files) { count++; new QTreeWidgetItem(others, QStringList() << file); } } } uint used = 0; uint unUsed = 0; qint64 usedSize = 0; qint64 unUsedSize = 0; pCore->bin()->getBinStats(&used, &unUsed, &usedSize, &unUsedSize); allFonts.removeDuplicates(); // Hide unused categories for (int i = 0; i < files_list->topLevelItemCount(); ++i) { if (files_list->topLevelItem(i)->childCount() == 0) { files_list->topLevelItem(i)->setHidden(true); } } files_count->setText(QString::number(count)); fonts_list->addItems(allFonts); if (allFonts.isEmpty()) { fonts_list->setHidden(true); label_fonts->setHidden(true); } used_count->setText(QString::number(used)); used_size->setText(KIO::convertSize(static_cast(usedSize))); unused_count->setText(QString::number(unUsed)); unused_size->setText(KIO::convertSize(static_cast(unUsedSize))); delete_unused->setEnabled(unUsed > 0); } const QString ProjectSettings::selectedPreview() const { return preview_profile->itemData(preview_profile->currentIndex()).toString(); } void ProjectSettings::accept() { if (selectedProfile().isEmpty()) { KMessageBox::error(this, i18n("Please select a video profile")); return; } QString params = preview_profile->itemData(preview_profile->currentIndex()).toString(); if (!params.isEmpty()) { if (params.section(QLatin1Char(';'), 0, 0) != m_previewparams || params.section(QLatin1Char(';'), 1, 1) != m_previewextension) { // Timeline preview settings changed, warn if (KMessageBox::warningContinueCancel(this, i18n("You changed the timeline preview profile. This will remove all existing timeline previews for " "this project.\n Are you sure you want to proceed?"), i18n("Confirm profile change")) == KMessageBox::Cancel) { return; } } } if (!m_savedProject && selectedProfile() != pCore->getCurrentProfile()->path()) { if (KMessageBox::warningContinueCancel( this, i18n("Changing the profile of your project cannot be undone.\nIt is recommended to save your project before attempting this operation " "that might cause some corruption in transitions.\n Are you sure you want to proceed?"), i18n("Confirm profile change")) == KMessageBox::Cancel) { return; } } QDialog::accept(); } void ProjectSettings::slotUpdateButton(const QString &path) { if (path.isEmpty()) { m_buttonOk->setEnabled(false); } else { m_buttonOk->setEnabled(true); slotUpdateFiles(true); } } QString ProjectSettings::selectedProfile() const { return m_pw->selectedProfile(); } QUrl ProjectSettings::selectedFolder() const { return project_folder->url(); } QPoint ProjectSettings::tracks() const { QPoint p; p.setX(video_tracks->value()); p.setY(audio_tracks->value()); return p; } bool ProjectSettings::enableVideoThumbs() const { return video_thumbs->isChecked(); } bool ProjectSettings::enableAudioThumbs() const { return audio_thumbs->isChecked(); } bool ProjectSettings::useProxy() const { return enable_proxy->isChecked(); } bool ProjectSettings::generateProxy() const { return generate_proxy->isChecked(); } bool ProjectSettings::generateImageProxy() const { return generate_imageproxy->isChecked(); } int ProjectSettings::proxyMinSize() const { return proxy_minsize->value(); } int ProjectSettings::proxyImageMinSize() const { return proxy_imageminsize->value(); } QString ProjectSettings::proxyParams() const { QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString(); return params.section(QLatin1Char(';'), 0, 0); } QString ProjectSettings::proxyExtension() const { QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString(); return params.section(QLatin1Char(';'), 1, 1); } // static QStringList ProjectSettings::extractPlaylistUrls(const QString &path) { QStringList urls; QDomDocument doc; QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return urls; } if (!doc.setContent(&file)) { file.close(); return urls; } file.close(); QString root = doc.documentElement().attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } QDomNodeList files = doc.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < files.count(); ++i) { QDomElement e = files.at(i).toElement(); QString type = EffectsList::property(e, QStringLiteral("mlt_service")); if (type != QLatin1String("colour")) { QString url = EffectsList::property(e, QStringLiteral("resource")); if (type == QLatin1String("timewarp")) { url = EffectsList::property(e, QStringLiteral("warp_resource")); } else if (type == QLatin1String("framebuffer")) { url = url.section(QLatin1Char('?'), 0, 0); } if (!url.isEmpty()) { if (QFileInfo(url).isRelative()) { url.prepend(root); } if (url.section(QLatin1Char('.'), 0, -2).endsWith(QLatin1String("/.all"))) { // slideshow clip, extract image urls urls << extractSlideshowUrls(url); } else { urls << url; } if (url.endsWith(QLatin1String(".mlt")) || url.endsWith(QLatin1String(".kdenlive"))) { // TODO: Do something to avoid infinite loops if 2 files reference themselves... urls << extractPlaylistUrls(url); } } } } // luma files for transitions files = doc.elementsByTagName(QStringLiteral("transition")); for (int i = 0; i < files.count(); ++i) { QDomElement e = files.at(i).toElement(); QString url = EffectsList::property(e, QStringLiteral("resource")); if (!url.isEmpty()) { if (QFileInfo(url).isRelative()) { url.prepend(root); } urls << url; } } return urls; } // static QStringList ProjectSettings::extractSlideshowUrls(const QString &url) { QStringList urls; QString path = QFileInfo(url).absolutePath(); QDir dir(path); if (url.contains(QStringLiteral(".all."))) { // this is a MIME slideshow, like *.jpeg QString ext = url.section(QLatin1Char('.'), -1); QStringList filters; filters << QStringLiteral("*.") + ext; dir.setNameFilters(filters); QStringList result = dir.entryList(QDir::Files); urls.append(path + filters.at(0) + QStringLiteral(" (") + i18np("1 image found", "%1 images found", result.count()) + QLatin1Char(')')); } else { // this is a pattern slideshow, like sequence%4d.jpg QString filter = QFileInfo(url).fileName(); QString ext = filter.section(QLatin1Char('.'), -1); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); int count = 0; const QStringList result = dir.entryList(QDir::Files); for (const QString &p : result) { if (rx.exactMatch(p)) { count++; } } urls.append(url + QStringLiteral(" (") + i18np("1 image found", "%1 images found", count) + QLatin1Char(')')); } return urls; } void ProjectSettings::slotExportToText() { const QString savePath = QFileDialog::getSaveFileName(this, QString(), project_folder->url().toLocalFile(), QStringLiteral("text/plain")); if (savePath.isEmpty()) { return; } QString text; text.append(i18n("Project folder: %1", project_folder->url().toLocalFile()) + '\n'); text.append(i18n("Project profile: %1", m_pw->selectedProfile()) + '\n'); text.append(i18n("Total clips: %1 (%2 used in timeline).", files_count->text(), used_count->text()) + "\n\n"); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { if (files_list->topLevelItem(i)->childCount() > 0) { text.append('\n' + files_list->topLevelItem(i)->text(0) + ":\n\n"); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { text.append(files_list->topLevelItem(i)->child(j)->text(0) + '\n'); } } } QTemporaryFile tmpfile; if (!tmpfile.open()) { qCWarning(KDENLIVE_LOG) << "///// CANNOT CREATE TMP FILE in: " << tmpfile.fileName(); return; } QFile xmlf(tmpfile.fileName()); if (!xmlf.open(QIODevice::WriteOnly)) { return; } xmlf.write(text.toUtf8()); if (xmlf.error() != QFile::NoError) { xmlf.close(); return; } xmlf.close(); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(tmpfile.fileName()), QUrl::fromLocalFile(savePath)); copyjob->exec(); } void ProjectSettings::slotUpdateProxyParams() { QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString(); proxyparams->setPlainText(params.section(QLatin1Char(';'), 0, 0)); } void ProjectSettings::slotUpdatePreviewParams() { QString params = preview_profile->itemData(preview_profile->currentIndex()).toString(); previewparams->setPlainText(params.section(QLatin1Char(';'), 0, 0)); } const QMap ProjectSettings::metadata() const { QMap metadata; for (int i = 0; i < metadata_list->topLevelItemCount(); ++i) { QTreeWidgetItem *item = metadata_list->topLevelItem(i); if (!item->text(1).simplified().isEmpty()) { // Insert metadata entry QString key = item->data(0, Qt::UserRole).toString(); if (key.isEmpty()) { key = QStringLiteral("meta.attr.") + item->text(0).simplified() + QStringLiteral(".markup"); } QString value = item->text(1); metadata.insert(key, value); } } return metadata; } void ProjectSettings::slotAddMetadataField() { QString metaField = QInputDialog::getText(this, i18n("Metadata"), i18n("Metadata")); if (metaField.isEmpty()) { return; } QTreeWidgetItem *item = new QTreeWidgetItem(metadata_list, QStringList() << metaField); item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } void ProjectSettings::slotDeleteMetadataField() { QTreeWidgetItem *item = metadata_list->currentItem(); if (item) { delete item; } } void ProjectSettings::slotManageEncodingProfile() { QPointer d = new EncodingProfilesDialog(0); d->exec(); delete d; loadProxyProfiles(); } void ProjectSettings::slotManagePreviewProfile() { QPointer d = new EncodingProfilesDialog(1); d->exec(); delete d; loadPreviewProfiles(); } void ProjectSettings::loadProxyProfiles() { // load proxy profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator k(values); int ix = -1; proxy_profile->clear(); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { QString params = k.value().section(QLatin1Char(';'), 0, 0); QString extension = k.value().section(QLatin1Char(';'), 1, 1); if (ix == -1 && ((params == m_proxyparameters && extension == m_proxyextension) || (m_proxyparameters.isEmpty() || m_proxyextension.isEmpty()))) { // this is the current profile ix = proxy_profile->count(); } proxy_profile->addItem(k.key(), k.value()); } } if (ix == -1) { // Current project proxy settings not found ix = proxy_profile->count(); proxy_profile->addItem(i18n("Current Settings"), QString(m_proxyparameters + QLatin1Char(';') + m_proxyextension)); } proxy_profile->setCurrentIndex(ix); slotUpdateProxyParams(); } void ProjectSettings::loadPreviewProfiles() { // load proxy profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap values = group.entryMap(); QMapIterator k(values); int ix = -1; preview_profile->clear(); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { QString params = k.value().section(QLatin1Char(';'), 0, 0); QString extension = k.value().section(QLatin1Char(';'), 1, 1); if (ix == -1 && (params == m_previewparams && extension == m_previewextension)) { // this is the current profile ix = preview_profile->count(); } preview_profile->addItem(k.key(), k.value()); } } if (ix == -1) { // Current project proxy settings not found ix = preview_profile->count(); if (m_previewparams.isEmpty() && m_previewextension.isEmpty()) { // Leave empty, will be automatically detected preview_profile->addItem(i18n("Auto")); } else { preview_profile->addItem(i18n("Current Settings"), QString(m_previewparams + QLatin1Char(';') + m_previewextension)); } } preview_profile->setCurrentIndex(ix); slotUpdatePreviewParams(); } const QString ProjectSettings::storageFolder() const { if (custom_folder->isChecked()) { return project_folder->url().toLocalFile(); } return QString(); } diff --git a/src/project/jobs/CMakeLists.txt b/src/project/jobs/CMakeLists.txt deleted file mode 100644 index 23fe93a1a..000000000 --- a/src/project/jobs/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -set(kdenlive_SRCS - ${kdenlive_SRCS} - project/jobs/abstractclipjob.cpp - project/jobs/proxyclipjob.cpp - project/jobs/cutclipjob.cpp - project/jobs/meltjob.cpp - project/jobs/filterjob.cpp - project/jobs/jobmanager.cpp - PARENT_SCOPE) diff --git a/src/project/jobs/jobmanager.cpp b/src/project/jobs/jobmanager.cpp deleted file mode 100644 index 4ddacfe85..000000000 --- a/src/project/jobs/jobmanager.cpp +++ /dev/null @@ -1,368 +0,0 @@ -/* -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 "jobmanager.h" -#include "abstractclipjob.h" -#include "bin/bin.h" -#include "bin/projectclip.h" -#include "cutclipjob.h" -#include "doc/kdenlivedoc.h" -#include "filterjob.h" -#include "kdenlivesettings.h" -#include "meltjob.h" -#include "mlt++/Mlt.h" -#include "project/clipstabilize.h" -#include "proxyclipjob.h" - -#include "kdenlive_debug.h" -#include - -#include "ui_scenecutdialog_ui.h" -#include -#include - -JobManager::JobManager(Bin *bin) - : QObject() - , m_bin(bin) - , m_abortAllJobs(false) -{ - connect(this, &JobManager::processLog, this, &JobManager::slotProcessLog); - connect(this, &JobManager::checkJobProcess, this, &JobManager::slotCheckJobProcess); -} - -JobManager::~JobManager() -{ - m_abortAllJobs = true; - for (int i = 0; i < m_jobList.count(); ++i) { - m_jobList.at(i)->setStatus(JobAborted); - } - m_jobThreads.waitForFinished(); - m_jobThreads.clearFutures(); - if (!m_jobList.isEmpty()) { - qDeleteAll(m_jobList); - } - m_jobList.clear(); -} - -void JobManager::slotProcessLog(const QString &id, int progress, int type, const QString &message) -{ - std::shared_ptr item = m_bin->getBinClip(id); - item->setJobStatus((AbstractClipJob::JOBTYPE)type, JobWorking, progress, message); -} - -QStringList JobManager::getPendingJobs(const QString &id) -{ - QStringList result; - QMutexLocker lock(&m_jobMutex); - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->clipId() == id && (m_jobList.at(i)->status() == JobWaiting || m_jobList.at(i)->status() == JobWorking)) { - // discard this job - result << m_jobList.at(i)->description; - } - } - return result; -} - -void JobManager::discardJobs(const QString &id, AbstractClipJob::JOBTYPE type) -{ - QMutexLocker lock(&m_jobMutex); - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->clipId() == id && (type == AbstractClipJob::NOJOBTYPE || m_jobList.at(i)->jobType == type)) { - // discard this job - m_jobList.at(i)->setStatus(JobAborted); - } - } - emit updateJobStatus(id, type, JobAborted); - updateJobCount(); -} - -bool JobManager::hasPendingJob(const QString &clipId, AbstractClipJob::JOBTYPE type) -{ - QMutexLocker lock(&m_jobMutex); - for (int i = 0; i < m_jobList.count(); i++) { - if (m_abortAllJobs) { - break; - } - AbstractClipJob *job = m_jobList.at(i); - if (job->clipId() == clipId && job->jobType == type && (job->status() == JobWaiting || job->status() == JobWorking)) { - return true; - } - } - return false; -} - -void JobManager::slotCheckJobProcess() -{ - if (!m_jobThreads.futures().isEmpty()) { - // Remove inactive threads - QList> futures = m_jobThreads.futures(); - m_jobThreads.clearFutures(); - for (int i = 0; i < futures.count(); ++i) - if (!futures.at(i).isFinished()) { - m_jobThreads.addFuture(futures.at(i)); - } - } - if (m_jobList.isEmpty()) { - return; - } - - m_jobMutex.lock(); - int count = 0; - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->status() == JobWorking || m_jobList.at(i)->status() == JobWaiting) { - count++; - } else { - // remove finished jobs - AbstractClipJob *job = m_jobList.takeAt(i); - job->deleteLater(); - --i; - } - } - m_jobMutex.unlock(); - emit jobCount(count); - if (m_jobThreads.futures().isEmpty() || m_jobThreads.futures().count() < KdenliveSettings::proxythreads()) { - m_jobThreads.addFuture(QtConcurrent::run(this, &JobManager::slotProcessJobs)); - } -} - -void JobManager::updateJobCount() -{ - int count = 0; - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->status() == JobWaiting || m_jobList.at(i)->status() == JobWorking) { - count++; - } - } - // Set jobs count - emit jobCount(count); -} - -void JobManager::slotProcessJobs() -{ - bool firstPass = true; - while (!m_jobList.isEmpty() && !m_abortAllJobs) { - AbstractClipJob *job = nullptr; - m_jobMutex.lock(); - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->status() == JobWaiting) { - job = m_jobList.at(i); - job->setStatus(JobWorking); - break; - } - } - if (!firstPass) { - updateJobCount(); - } - m_jobMutex.unlock(); - - if (job == nullptr) { - break; - } - firstPass = false; - QString destination = job->destination(); - // Check if the clip is still here - std::shared_ptr currentClip = m_bin->getBinClip(job->clipId()); - if (currentClip == nullptr) { - job->setStatus(JobDone); - continue; - } - // Set clip status to started - currentClip->setJobStatus(job->jobType, job->status()); - - // Make sure destination path is writable - if (!destination.isEmpty()) { - QFileInfo file(destination); - bool writable = false; - if (file.exists()) { - if (file.isWritable()) { - writable = true; - } - } else { - QDir dir = file.absoluteDir(); - if (!dir.exists()) { - writable = dir.mkpath(QStringLiteral(".")); - } else { - QFileInfo dinfo(dir.absolutePath()); - writable = dinfo.isWritable(); - } - } - if (!writable) { - emit updateJobStatus(job->clipId(), job->jobType, JobCrashed, i18n("Cannot write to path: %1", destination)); - job->setStatus(JobCrashed); - continue; - } - } - connect(job, SIGNAL(jobProgress(QString, int, int)), this, SIGNAL(processLog(QString, int, int))); - connect(job, &AbstractClipJob::cancelRunningJob, m_bin, &Bin::slotCancelRunningJob); - - if (job->jobType == AbstractClipJob::MLTJOB || job->jobType == AbstractClipJob::ANALYSECLIPJOB) { - connect(job, SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap)), this, - SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap))); - } - job->startJob(); - if (job->status() == JobDone) { - emit updateJobStatus(job->clipId(), job->jobType, JobDone); - // TODO: replace with more generic clip replacement framework - if (job->jobType == AbstractClipJob::PROXYJOB) { - m_bin->gotProxy(job->clipId(), destination); - } else if (job->addClipToProject() > -100) { - emit addClip(destination, job->addClipToProject()); - } - } else if (job->status() == JobCrashed || job->status() == JobAborted) { - emit updateJobStatus(job->clipId(), job->jobType, job->status(), job->errorMessage(), QString(), job->logDetails()); - } - } - // Thread finished, cleanup & update count - QTimer::singleShot(200, this, &JobManager::checkJobProcess); -} - -QList JobManager::filterClips(const QList &clips, AbstractClipJob::JOBTYPE jobType, const QStringList ¶ms) -{ - // TODO: filter depending on clip type - if (jobType == AbstractClipJob::TRANSCODEJOB || jobType == AbstractClipJob::CUTJOB) { - return CutClipJob::filterClips(clips, params); - } - if (jobType == AbstractClipJob::FILTERCLIPJOB) { - return FilterJob::filterClips(clips, params); - } - if (jobType == AbstractClipJob::PROXYJOB) { - return ProxyJob::filterClips(clips); - } - return QList(); -} - -void JobManager::prepareJobFromTimeline(ProjectClip *clip, const QMap &producerParams, const QMap &filterParams, - const QMap &consumerParams, const QMap &extraParams) -{ - auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); - job->description = i18n("Filter %1", extraParams.value(QStringLiteral("finalfilter"))); - launchJob(clip, job); -} - -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::launchJob(ProjectClip *clip, AbstractClipJob *job, bool runQueue) -{ - if (job->isExclusive() && hasPendingJob(clip->AbstractProjectItem::clipId(), job->jobType)) { - delete job; - return; - } - - m_jobList.append(job); - clip->setJobStatus(job->jobType, JobWaiting, 0, job->statusMessage()); - if (runQueue) { - slotCheckJobProcess(); - } -} - -void JobManager::slotDiscardClipJobs() -{ - QAction *act = qobject_cast(sender()); - if (act == nullptr) { - // Cannot access triggering action, something is wrong - qCDebug(KDENLIVE_LOG) << "// Error in job action"; - return; - } - QString id = act->data().toString(); - if (id.isEmpty()) { - return; - } - discardJobs(id); -} - -void JobManager::slotCancelPendingJobs() -{ - QMutexLocker lock(&m_jobMutex); - for (int i = 0; i < m_jobList.count(); ++i) { - if (m_jobList.at(i)->status() == JobWaiting) { - // discard this job - m_jobList.at(i)->setStatus(JobAborted); - emit updateJobStatus(m_jobList.at(i)->clipId(), m_jobList.at(i)->jobType, JobAborted); - } - } - updateJobCount(); -} - -void JobManager::slotCancelJobs() -{ - m_abortAllJobs = true; - for (int i = 0; i < m_jobList.count(); ++i) { - m_jobList.at(i)->setStatus(JobAborted); - } - m_jobThreads.waitForFinished(); - m_jobThreads.clearFutures(); - - // TODO: undo job cancelation ? not sure it's necessary - /*QUndoCommand *command = new QUndoCommand(); - command->setText(i18np("Cancel job", "Cancel jobs", m_jobList.count())); - m_jobMutex.lock(); - for (int i = 0; i < m_jobList.count(); ++i) { - DocClipBase *currentClip = m_doc->clipManager()->getClipById(m_jobList.at(i)->clipId()); - if (!currentClip) continue; - QMap newProps = m_jobList.at(i)->cancelProperties(); - if (newProps.isEmpty()) continue; - QMap oldProps = currentClip->currentProperties(newProps); - //TODO - //new EditClipCommand(this, m_jobList.at(i)->clipId(), oldProps, newProps, true, command); - } - m_jobMutex.unlock(); - if (command->childCount() > 0) { - m_doc->commandStack()->push(command); - } - else delete command; - */ - if (!m_jobList.isEmpty()) { - qDeleteAll(m_jobList); - } - m_jobList.clear(); - m_abortAllJobs = false; - emit jobCount(0); -} diff --git a/src/project/jobs/jobmanager.h b/src/project/jobs/jobmanager.h deleted file mode 100644 index dcdae3a69..000000000 --- a/src/project/jobs/jobmanager.h +++ /dev/null @@ -1,136 +0,0 @@ -/* -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 JOBMANAGER -#define JOBMANAGER - -#include "abstractclipjob.h" -#include "definitions.h" - -#include -#include -#include - -class AbstractClipJob; -class Bin; -class ProjectClip; - -/** - * @class JobManager - * @brief This class is responsible for clip jobs management. - * - */ - -class JobManager : public QObject -{ - Q_OBJECT - -public: - explicit JobManager(Bin *bin); - virtual ~JobManager(); - - /** @brief Discard specific job type for a clip. - * @param id the clip id - * @param type The type of job that you want to abort, leave to NOJOBTYPE to abort all jobs - */ - void discardJobs(const QString &id, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); - - /** @brief Check if there is a pending / running job a clip. - * @param id the clip id - * @param type The type of job that you want to query - */ - bool hasPendingJob(const QString &clipId, AbstractClipJob::JOBTYPE type); - - /** @brief Launch an MLT filter job that was requested by an effect applied on a timeline clip - * @param clip the bin clip - * @param producerParams the parameters for the MLT Producer - * @param filterParams the parameters for the MLT Filter - * @param consumerParams the parameters for the MLT consumer - * @param extraParams parameters that will tell the job what to do when finished - */ - void prepareJobFromTimeline(ProjectClip *clip, const QMap &producerParams, const QMap &filterParams, - const QMap &consumerParams, const QMap &extraParams); - - /** @brief Get ready to process selected job - * @param clips the list of selected clips - * @param jobType the jobtype requested - * @param type the parameters for the job - */ - void prepareJobs(const QList &clips, double fps, AbstractClipJob::JOBTYPE jobType, const QStringList ¶ms = QStringList()); - - /** @brief Filter a list of selected clips to keep only those that match the job type - * @param clips the list of selected clips - * @param jobType the jobtype requested - * @param type the parameters for the job, might be relevant to filter clips - * @returns A QMap list (id, urls) of clip that match the job type - */ - QList filterClips(const QList &clips, AbstractClipJob::JOBTYPE jobType, const QStringList ¶ms); - - /** @brief Put the job in our queue. - * @param clip the clip to whom the job will be applied - * @param job the job - * @param runQueue If true, try to start the job right now. False waits for a later command to start processing, useful when adding many jobs quickly. - */ - void launchJob(ProjectClip *clip, AbstractClipJob *job, bool runQueue = true); - - /** @brief Get the list of job names for current clip. */ - QStringList getPendingJobs(const QString &id); - -private slots: - void slotCheckJobProcess(); - void slotProcessJobs(); - void slotProcessLog(const QString &id, int progress, int type, const QString &message); - -public slots: - /** @brief Discard jobs running on a clip whose id is in the calling action's data. */ - void slotDiscardClipJobs(); - /** @brief Discard all running jobs. */ - void slotCancelJobs(); - /** @brief Discard all pending jobs. */ - void slotCancelPendingJobs(); - -private: - /** @brief A pointer to the project's bin. */ - Bin *m_bin; - /** @brief Mutex preventing thread issues. */ - QMutex m_jobMutex; - /** @brief Holds a list of active jobs. */ - QList m_jobList; - /** @brief Holds the threads running a job. */ - QFutureSynchronizer m_jobThreads; - /** @brief Set to true to trigger abortion of all jobs. */ - bool m_abortAllJobs; - /** @brief Create a proxy for a clip. */ - void createProxy(const QString &id); - /** @brief Update job count in info widget. */ - void updateJobCount(); - -signals: - void addClip(const QString &, int folderId); - void processLog(const QString &, int, int, const QString & = QString()); - void updateJobStatus(const QString &, int, int, const QString &label = QString(), const QString &actionName = QString(), - const QString &details = QString()); - void gotFilterJobResults(const QString &, int, int, stringMap, stringMap); - void jobCount(int); - void checkJobProcess(); -}; - -#endif diff --git a/src/project/jobs/meltjob.h b/src/project/jobs/meltjob.h deleted file mode 100644 index 973b8eb95..000000000 --- a/src/project/jobs/meltjob.h +++ /dev/null @@ -1,92 +0,0 @@ -/*************************************************************************** - * * - * 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 MELTJOB -#define MELTJOB - -#include "abstractclipjob.h" - -namespace Mlt { -class Profile; -class Producer; -class Consumer; -class Filter; -class Event; -} - -/** - * @class MeltJob - * @brief This class contains a Job that will run an MLT Producer, with some optional filter - * - */ - -class MeltJob : public AbstractClipJob -{ - Q_OBJECT - -public: - /** @brief Creates the Job. - * @param cType the Clip Type (AV, PLAYLIST, AUDIO, ...) as defined in definitions.h. Some jobs will act differently depending on clip type - * @param id the id of the clip that requested this clip job - * @param producerParams the parameters that will be passed to the Mlt::Producer. The "producer" value will be used to initialize the producer. - * should contain the in and out values (in=0, out=-1 to process all clip) - * @param filterParams the parameters that will be passed to the (optional) Mlt::Filter attached to the producer. The "filter" value - * should contain the MLT's filter name. - * @param consumerParams the parameters passed to the Mlt::Consumer. The "consumer" value should hold the consumer's initialization string. - @param extraParams these parameters can be used to further affect the Job handling. - */ - MeltJob(ClipType cType, const QString &id, const QMap &producerParams, const QMap &filterParams, - const QMap &consumerParams, const stringMap &extraParams = stringMap()); - virtual ~MeltJob(); - /** @brief Returns the file path that will be written by this Mlt job. Empty when no file is written. */ - const QString destination() const override; - /** @brief Start processing the job. */ - void startJob() override; - /** @brief These properties can be used to undo the action that launched this job. */ - stringMap cancelProperties() override; - /** @brief When true, this will tell the JobManager to add the @destination() file to the project Bin. */ - bool addClipToProject; - /** @brief Returns a text string describing the job's current activity. */ - const QString statusMessage() override; - /** @brief Sets the status for this job (can be used by the JobManager to abort the job). */ - void setStatus(ClipJobStatus status) override; - /** @brief Here we will send the current progress info to anyone interested. */ - void emitFrameNumber(int pos); - -private: - Mlt::Consumer *m_consumer; - Mlt::Producer *m_producer; - Mlt::Profile *m_profile; - Mlt::Filter *m_filter; - Mlt::Event *m_showFrameEvent; - QMap m_producerParams; - QMap m_filterParams; - QMap m_consumerParams; - QString m_dest; - QString m_url; - int m_length; - QMap m_extra; - -signals: - /** @brief When user requested a to process an Mlt::Filter, this will send back all necessary infos. */ - void gotFilterJobResults(const QString &id, int startPos, int track, const stringMap &result, const stringMap &extra); -}; - -#endif diff --git a/src/project/projectmanager.cpp b/src/project/projectmanager.cpp index 00f7e00a4..4ccb6cdf7 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -1,901 +1,902 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "projectmanager.h" #include "bin/bin.h" #include "core.h" #include "doc/kdenlivedoc.h" +#include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/producerqueue.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/archivewidget.h" #include "project/dialogs/backupwidget.h" #include "project/dialogs/noteswidget.h" #include "project/dialogs/projectsettings.h" // Temporary for testing #include "bin/model/markerlistmodel.hpp" #include "project/notesplugin.h" #include "timeline2/model/builders/meltBuilder.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include "utils/KoIconUtils.h" #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ProjectManager::ProjectManager(QObject *parent) : QObject(parent) , m_project(nullptr) , m_progressDialog(nullptr) { m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection()); m_fileRevert->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-revert"))); m_fileRevert->setEnabled(false); QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-open"))); a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-save-as"))); a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-new"))); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection()); QAction *backupAction = new QAction(KoIconUtils::themedIcon(QStringLiteral("edit-undo")), i18n("Open Backup File"), this); pCore->window()->addAction(QStringLiteral("open_backup"), backupAction); connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup())); m_notesPlugin = new NotesPlugin(this); m_autoSaveTimer.setSingleShot(true); connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave); // Ensure the default data folder exist QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.mkpath(QStringLiteral(".backup")); dir.mkdir(QStringLiteral("titles")); } -ProjectManager::~ProjectManager() -{ -} +ProjectManager::~ProjectManager() {} void ProjectManager::slotLoadOnOpen() { if (m_startUrl.isValid()) { openFile(); } else if (KdenliveSettings::openlastproject()) { openLastFile(); } else { newFile(false); } if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) { const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(',')); QList urls; urls.reserve(list.count()); for (const QString &path : list) { // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path); urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path)); } pCore->bin()->droppedUrls(urls); } m_loadClipsOnOpen.clear(); } void ProjectManager::init(const QUrl &projectUrl, const QString &clipList) { m_startUrl = projectUrl; m_loadClipsOnOpen = clipList; } void ProjectManager::newFile(bool showProjectSettings, bool force) { // fix mantis#3160 QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive")); if (checkForBackupFile(startFile)) { return; } m_fileRevert->setEnabled(false); QString profileName = KdenliveSettings::default_profile(); if (profileName.isEmpty()) { profileName = pCore->getCurrentProfile()->path(); } QString projectFolder; QMap documentProperties; QMap documentMetadata; QPoint projectTracks(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()); pCore->monitorManager()->resetDisplay(); QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch()); documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); documentProperties.insert(QStringLiteral("documentid"), documentId); if (!showProjectSettings) { if (!closeCurrentDocument()) { return; } if (KdenliveSettings::customprojectfolder()) { projectFolder = KdenliveSettings::defaultprojectfolder(); if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } } else { QPointer w = new ProjectSettings(nullptr, QMap(), QStringList(), projectTracks.x(), projectTracks.y(), KdenliveSettings::defaultprojectfolder(), false, true, pCore->window()); connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles); if (w->exec() != QDialog::Accepted) { delete w; return; } if (!closeCurrentDocument()) { delete w; return; } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { pCore->window()->slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { pCore->window()->slotSwitchAudioThumbs(); } profileName = w->selectedProfile(); projectFolder = w->storageFolder(); projectTracks = w->tracks(); documentProperties.insert(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); documentProperties.insert(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams()); documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension()); documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); QString preview = w->selectedPreview(); if (!preview.isEmpty()) { documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0)); documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1)); } documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); if (!projectFolder.isEmpty()) { if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } documentMetadata = w->metadata(); delete w; } bool openBackup; m_notesPlugin->clear(); KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, &openBackup, pCore->window()); doc->m_autosave = new KAutoSaveFile(startFile, doc); pCore->bin()->setDocument(doc); // TODO REFAC: Delete this /*QList rulerActions; rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone")); m_trackView = new Timeline(doc, pCore->window()->kdenliveCategoryMap.value(QStringLiteral("timeline"))->actions(), rulerActions, &ok, pCore->window()); // Set default target tracks to upper audio / lower video tracks m_trackView->audioTarget = projectTracks.y() > 0 ? projectTracks.y() : -1; m_trackView->videoTarget = projectTracks.x() > 0 ? projectTracks.y() + 1 : -1; connect(m_trackView->projectView(), SIGNAL(importPlaylistClips(ItemInfo, QString, QUndoCommand *)), pCore->bin(), SLOT(slotExpandUrl(ItemInfo, QString, QUndoCommand *)), Qt::DirectConnection); m_trackView->loadTimeline(); pCore->window()->m_timelineArea->addTab(m_trackView, QIcon::fromTheme(QStringLiteral("kdenlive")), doc->description());*/ // END of things to delete m_project = doc; updateTimeline(); /*if (!ok) { // MLT is broken //pCore->window()->m_timelineArea->setEnabled(false); //pCore->window()->m_projectList->setEnabled(false); pCore->window()->slotPreferences(6); return; }*/ pCore->window()->connectDocument(); bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1"); QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects")); if (disableEffects) { if (disabled != disableEffects->isChecked()) { disableEffects->blockSignals(true); disableEffects->setChecked(disabled); disableEffects->blockSignals(false); } } emit docOpened(m_project); - //pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); + // pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); m_lastSave.start(); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor, true); } bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) { if ((m_project != nullptr) && m_project->isModified() && saveChanges) { QString message; if (m_project->url().fileName().isEmpty()) { message = i18n("Save changes to document?"); } else { message = i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName()); } switch (KMessageBox::warningYesNoCancel(pCore->window(), message)) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { return false; } break; case KMessageBox::Cancel: return false; break; default: break; } } if (!quit && !qApp->isSavingSession()) { m_autoSaveTimer.stop(); if (m_project) { - pCore->producerQueue()->abortOperations(); + pCore->jobManager()->slotCancelJobs(); pCore->bin()->abortOperations(); pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr); pCore->window()->clearAssetPanel(); delete m_project; m_project = nullptr; } pCore->monitorManager()->setDocument(m_project); } return true; } bool ProjectManager::saveFileAs(const QString &outputFileName) { pCore->monitorManager()->pauseActiveMonitor(); // Sync document properties prepareSave(); QString saveFolder = QFileInfo(outputFileName).absolutePath(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } if (!m_project->saveSceneList(outputFileName, scene)) { return false; } QUrl url = QUrl::fromLocalFile(outputFileName); // Save timeline thumbnails // m_trackView->projectView()->saveThumbnails(); m_project->setUrl(url); // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/ // saved under file name // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited // This timer is set by KdenliveDoc::setModified() if (m_project->m_autosave == nullptr) { // The temporary file is not opened or created until actually needed. // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched). m_project->m_autosave = new KAutoSaveFile(url, this); } else { m_project->m_autosave->setManagedFile(url); } pCore->window()->setWindowTitle(m_project->description()); m_project->setModified(false); m_recentFilesAction->addUrl(url); // remember folder for next project opening KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder); saveRecentFiles(); m_fileRevert->setEnabled(true); pCore->window()->m_undoView->stack()->setClean(); return true; } void ProjectManager::saveRecentFiles() { KSharedConfigPtr config = KSharedConfig::openConfig(); m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files")); config->sync(); } void ProjectManager::slotSaveSelection(const QString &path) { // TODO refac : look at this - //m_trackView->projectView()->exportTimelineSelection(path); + // m_trackView->projectView()->exportTimelineSelection(path); } bool ProjectManager::hasSelection() const { return false; // TODO refac : look at this - //return m_trackView->projectView()->hasSelection(); + // return m_trackView->projectView()->hasSelection(); } bool ProjectManager::saveFileAs() { QFileDialog fd(pCore->window()); fd.setDirectory(m_project->url().isValid() ? m_project->url().adjusted(QUrl::RemoveFilename).toLocalFile() : KdenliveSettings::defaultprojectfolder()); fd.setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlive")); fd.setAcceptMode(QFileDialog::AcceptSave); fd.setFileMode(QFileDialog::AnyFile); fd.setDefaultSuffix(QStringLiteral("kdenlive")); if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) { return false; } QString outputFile = fd.selectedFiles().constFirst(); #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Since Plasma 5.7 (release at same time as KF 5.23, // the file dialog manages the overwrite check if (QFile::exists(outputFile)) { // Show the file dialog again if the user does not want to overwrite the file if (KMessageBox::questionYesNo(pCore->window(), i18n("File %1 already exists.\nDo you want to overwrite it?", outputFile)) == KMessageBox::No) { return saveFileAs(); } } #endif bool ok = false; QDir cacheDir = m_project->getCacheDir(CacheBase, &ok); if (ok) { QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile)))); file.open(QIODevice::ReadWrite | QIODevice::Text); file.close(); } return saveFileAs(outputFile); } bool ProjectManager::saveFile() { if (!m_project) { // Calling saveFile before a project was created, something is wrong qCDebug(KDENLIVE_LOG) << "SaveFile called without project"; return false; } if (m_project->url().isEmpty()) { return saveFileAs(); } bool result = saveFileAs(m_project->url().toLocalFile()); m_project->m_autosave->resize(0); return result; } void ProjectManager::openFile() { if (m_startUrl.isValid()) { openFile(m_startUrl); m_startUrl.clear(); return; } QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))), getMimeType()); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); m_recentFilesAction->addUrl(url); saveRecentFiles(); openFile(url); } void ProjectManager::openLastFile() { if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) { // No files in history newFile(false); return; } QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last(); if (firstUrlAction) { firstUrlAction->trigger(); } else { newFile(false); } } // fix mantis#3160 separate check from openFile() so we can call it from newFile() // to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it bool ProjectManager::checkForBackupFile(const QUrl &url) { // Check for autosave file that belong to the url we passed in. QList staleFiles = KAutoSaveFile::staleFiles(url); KAutoSaveFile *orphanedFile = nullptr; // Check if we can have a lock on one of the file, // meaning it is not handled by any Kdenlive instancce if (!staleFiles.isEmpty()) { for (KAutoSaveFile *stale : staleFiles) { if (stale->open(QIODevice::QIODevice::ReadWrite)) { // Found orphaned autosave file orphanedFile = stale; break; } else { // Another Kdenlive instance is probably handling this autosave file staleFiles.removeAll(stale); delete stale; continue; } } } if (orphanedFile) { if (KMessageBox::questionYesNo(nullptr, i18n("Auto-saved files exist. Do you want to recover them now?"), i18n("File Recovery"), KGuiItem(i18n("Recover")), KGuiItem(i18n("Don't recover"))) == KMessageBox::Yes) { doOpenFile(url, orphanedFile); return true; } // remove the stale files for (KAutoSaveFile *stale : staleFiles) { stale->open(QIODevice::ReadWrite); delete stale; } return false; } return false; } void ProjectManager::openFile(const QUrl &url) { QMimeDatabase db; // Make sure the url is a Kdenlive project file QMimeType mime = db.mimeTypeForUrl(url); if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) { // Opening a compressed project file, we need to process it // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing"; QPointer ar = new ArchiveWidget(url); if (ar->exec() == QDialog::Accepted) { openFile(QUrl::fromLocalFile(ar->extractedProjectFile())); } else if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } delete ar; return; } /*if (!url.fileName().endsWith(".kdenlive")) { // This is not a Kdenlive project file, abort loading KMessageBox::sorry(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile())); if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } return; }*/ if ((m_project != nullptr) && m_project->url() == url) { return; } if (!closeCurrentDocument()) { return; } if (checkForBackupFile(url)) { return; } pCore->window()->slotGotProgressInfo(i18n("Opening file %1", url.toLocalFile()), 100, InformationMessage); doOpenFile(url, nullptr); } void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale) { Q_ASSERT(m_project == nullptr); m_fileRevert->setEnabled(true); delete m_progressDialog; pCore->monitorManager()->resetDisplay(); m_progressDialog = new QProgressDialog(pCore->window()); m_progressDialog->setWindowTitle(i18n("Loading project")); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setLabelText(i18n("Loading project")); m_progressDialog->setMaximum(0); m_progressDialog->show(); bool openBackup; m_notesPlugin->clear(); KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack, KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(), QMap(), QMap(), QPoint(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()), &openBackup, pCore->window()); if (stale == nullptr) { stale = new KAutoSaveFile(url, doc); doc->m_autosave = stale; } else { doc->m_autosave = stale; stale->setParent(doc); // if loading from an autosave of unnamed file then keep unnamed if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) { doc->setUrl(QUrl()); } else { doc->setUrl(url); } doc->setModified(true); stale->setParent(doc); } m_progressDialog->setLabelText(i18n("Loading clips")); + + // TODO refac delete this pCore->bin()->setDocument(doc); QList rulerActions; rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("clear_render_timeline_zone")); /*bool ok; m_trackView = new Timeline(doc, pCore->window()->kdenliveCategoryMap.value(QStringLiteral("timeline"))->actions(), rulerActions, &ok, pCore->window()); connect(m_trackView, &Timeline::startLoadingBin, m_progressDialog, &QProgressDialog::setMaximum, Qt::DirectConnection); connect(m_trackView, &Timeline::resetUsageCount, pCore->bin(), &Bin::resetUsageCount, Qt::DirectConnection); connect(m_trackView, &Timeline::loadingBin, m_progressDialog, &QProgressDialog::setValue, Qt::DirectConnection);*/ // Set default target tracks to upper audio / lower video tracks m_project = doc; /*m_trackView->audioTarget = doc->getDocumentProperty(QStringLiteral("audiotargettrack"), QStringLiteral("-1")).toInt(); m_trackView->videoTarget = doc->getDocumentProperty(QStringLiteral("videotargettrack"), QStringLiteral("-1")).toInt(); m_trackView->loadTimeline(); m_trackView->loadGuides(pCore->binController()->takeGuidesData()); connect(m_trackView->projectView(), SIGNAL(importPlaylistClips(ItemInfo, QString, QUndoCommand *)), pCore->bin(), SLOT(slotExpandUrl(ItemInfo, QString, QUndoCommand *)), Qt::DirectConnection); bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1"); QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects")); if (disableEffects) { if (disabled != disableEffects->isChecked()) { disableEffects->blockSignals(true); disableEffects->setChecked(disabled); disableEffects->blockSignals(false); } }*/ updateTimeline(m_project->getDocumentProperty("position").toInt()); pCore->window()->connectDocument(); QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified(); - pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")), m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate, m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt()); + pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")), + m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate, + m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt()); emit docOpened(m_project); /*pCore->window()->m_timelineArea->setCurrentIndex(pCore->window()->m_timelineArea->addTab(m_trackView, QIcon::fromTheme(QStringLiteral("kdenlive")), m_project->description())); if (!ok) { pCore->window()->m_timelineArea->setEnabled(false); KMessageBox::sorry(pCore->window(), i18n("Cannot open file %1.\nProject is corrupted.", url.toLocalFile())); pCore->window()->slotGotProgressInfo(QString(), 100); newFile(false, true); return; } m_trackView->setDuration(m_trackView->duration());*/ pCore->window()->slotGotProgressInfo(QString(), 100); pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, m_project->getGuideModel()); if (openBackup) { slotOpenBackup(url); } m_lastSave.start(); delete m_progressDialog; m_progressDialog = nullptr; pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor, true); - //pCore->monitorManager()->projectMonitor()->refreshMonitorIfActive(); + // pCore->monitorManager()->projectMonitor()->refreshMonitorIfActive(); } void ProjectManager::adjustProjectDuration() { pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, m_project->getGuideModel()); } void ProjectManager::slotRevert() { if (m_project->isModified() && KMessageBox::warningContinueCancel(pCore->window(), i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"), i18n("Revert to last saved version")) == KMessageBox::Cancel) { return; } QUrl url = m_project->url(); if (closeCurrentDocument(false)) { doOpenFile(url, nullptr); } } QString ProjectManager::getMimeType(bool open) { QString mimetype = i18n("Kdenlive project (*.kdenlive)"); if (open) { mimetype.append(QStringLiteral(";;") + i18n("Archived project (*.tar.gz)")); } return mimetype; } KdenliveDoc *ProjectManager::current() { return m_project; } void ProjectManager::slotOpenBackup(const QUrl &url) { QUrl projectFile; QUrl projectFolder; QString projectId; if (url.isValid()) { // we could not open the project file, guess where the backups are projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder()); projectFile = url; } else { projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder()); projectFile = m_project->url(); projectId = m_project->getDocumentProperty(QStringLiteral("documentid")); } QPointer dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window()); if (dia->exec() == QDialog::Accepted) { QString requestedBackup = dia->selectedFile(); m_project->backupLastSavedVersion(projectFile.toLocalFile()); closeCurrentDocument(false); doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr); if (m_project) { m_project->setUrl(projectFile); m_project->setModified(true); pCore->window()->setWindowTitle(m_project->description()); } } delete dia; } - KRecentFilesAction *ProjectManager::recentFilesAction() { return m_recentFilesAction; } void ProjectManager::slotStartAutoSave() { // TODO REFAC: port to new timeline return; if (m_lastSave.elapsed() > 300000) { // If the project was not saved in the last 5 minute, force save m_autoSaveTimer.stop(); slotAutoSave(); } else { m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds } } void ProjectManager::slotAutoSave() { // TODO refac: repair this /* prepareSave(); bool multitrackEnabled = m_trackView->multitrackView; if (multitrackEnabled) { // Multitrack view was enabled, disable for auto save m_trackView->slotMultitrackView(false); } m_trackView->connectOverlayTrack(false); m_project->slotAutoSave(); m_trackView->connectOverlayTrack(true); if (multitrackEnabled) { // Multitrack view was enabled, re-enable for auto save m_trackView->slotMultitrackView(true); } m_lastSave.start(); */ } QString ProjectManager::projectSceneList(const QString &outputFolder) { // TODO: re-implement overlay and all // TODO refac: repair this return pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); /*bool multitrackEnabled = m_trackView->multitrackView; if (multitrackEnabled) { // Multitrack view was enabled, disable for auto save m_trackView->slotMultitrackView(false); } m_trackView->connectOverlayTrack(false); QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); m_trackView->connectOverlayTrack(true); if (multitrackEnabled) { // Multitrack view was enabled, re-enable for auto save m_trackView->slotMultitrackView(true); } return scene; */ } void ProjectManager::setDocumentNotes(const QString ¬es) { m_notesPlugin->widget()->setHtml(notes); } QString ProjectManager::documentNotes() const { QString text = m_notesPlugin->widget()->toPlainText().simplified(); if (text.isEmpty()) { return QString(); } return m_notesPlugin->widget()->toHtml(); } void ProjectManager::prepareSave() { - pCore->binController()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(), m_project->getGuideModel()); + pCore->binController()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(), + m_project->getGuideModel()); pCore->binController()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes()); pCore->binController()->saveProperty(QStringLiteral("kdenlive:docproperties.groups"), m_mainTimelineModel->groupsData()); } void ProjectManager::slotResetProfiles() { m_project->resetProfile(); pCore->monitorManager()->resetProfiles(m_project->timecode()); pCore->monitorManager()->updateScopeSource(); } void ProjectManager::slotExpandClip() { - //TODO refac - //m_trackView->projectView()->expandActiveClip(); + // TODO refac + // m_trackView->projectView()->expandActiveClip(); } void ProjectManager::disableBinEffects(bool disable) { if (m_project) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString()); } } pCore->monitorManager()->refreshProjectMonitor(); pCore->monitorManager()->refreshClipMonitor(); } void ProjectManager::slotDisableTimelineEffects(bool disable) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString()); } m_mainTimelineModel->setTimelineEffectsEnabled(!disable); pCore->monitorManager()->refreshProjectMonitor(); } void ProjectManager::slotSwitchTrackLock() { // TODO refac - //m_trackView->projectView()->switchTrackLock(); + // m_trackView->projectView()->switchTrackLock(); } void ProjectManager::slotSwitchAllTrackLock() { // TODO refac - //m_trackView->projectView()->switchAllTrackLock(); + // m_trackView->projectView()->switchAllTrackLock(); } void ProjectManager::slotSwitchTrackTarget() { // TODO refac - //m_trackView->switchTrackTarget(); + // m_trackView->switchTrackTarget(); } QString ProjectManager::getDefaultProjectFormat() { // On first run, lets use an HD1080p profile with fps related to timezone country. Then, when the first video is added to a project, if it does not match // our profile, propose a new default. QTimeZone zone; zone = QTimeZone::systemTimeZone(); QList ntscCountries; ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador; ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines; ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates; bool ntscProject = ntscCountries.contains(zone.country()); if (!ntscProject) { return QStringLiteral("atsc_1080p_25"); } return QStringLiteral("atsc_1080p_2997"); } void ProjectManager::saveZone(const QStringList &info, const QDir &dir) { pCore->bin()->saveZone(info, dir); } void ProjectManager::moveProjectData(const QString &src, const QString &dest) { // Move tmp folder (thumbnails, timeline preview) KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest)); connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished); connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotMoveProgress(KJob *, ulong))); m_project->moveProjectData(src, dest); } void ProjectManager::slotMoveProgress(KJob *, unsigned long progress) { pCore->window()->slotGotProgressInfo(i18n("Moving project folder"), static_cast(progress), ProcessingJobMessage); } void ProjectManager::slotMoveFinished(KJob *job) { if (job->error() == 0) { pCore->window()->slotGotProgressInfo(QString(), 100, InformationMessage); KIO::CopyJob *copyJob = static_cast(job); QString newFolder = copyJob->destUrl().toLocalFile(); // Check if project folder is inside document folder, in which case, paths will be relative QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme)); QDir srcDir(m_project->projectTempFolder()); if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) { m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/")); } else { m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/")); } m_project->setProjectFolder(QUrl::fromLocalFile(newFolder)); saveFile(); m_replacementPattern.clear(); slotRevert(); } else { KMessageBox::sorry(pCore->window(), i18n("Error moving project folder: %1", job->errorText())); } } void ProjectManager::updateTimeline(int pos) { - pCore->producerQueue()->abortOperations(); + pCore->jobManager()->slotCancelJobs(); // qDebug() << "Loading xml"<getProjectXml().constData(); QScopedPointer xmlProd(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", m_project->getProjectXml().constData())); Mlt::Service s(*xmlProd); Mlt::Tractor tractor(s); m_mainTimelineModel = TimelineItemModel::construct(&pCore->getCurrentProfile()->profile(), m_project->getGuideModel(), m_project->commandStack()); constructTimelineFromMelt(m_mainTimelineModel, tractor); const QString groupsData = m_project->getDocumentProperty(QStringLiteral("groups")); if (!groupsData.isEmpty()) { m_mainTimelineModel->loadGroups(groupsData); } - m_project->loadThumbs(); pCore->monitorManager()->projectMonitor()->setProducer(m_mainTimelineModel->producer(), pos); pCore->window()->getMainTimeline()->setModel(m_mainTimelineModel); m_mainTimelineModel->setUndoStack(m_project->commandStack()); } void ProjectManager::activateAsset(const QVariantMap effectData) { if (pCore->monitorManager()->projectMonitor()->isActive()) { pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } else { QString effect = effectData.value(QStringLiteral("kdenlive/effect")).toString(); QStringList effectString; effectString << effect; pCore->bin()->slotAddEffect(QString(), effectString); } } - std::shared_ptr ProjectManager::getGuideModel() { return current()->getGuideModel(); } std::shared_ptr ProjectManager::undoStack() { return current()->commandStack(); } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 029e7e339..e6e3a199a 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -1,15 +1,16 @@ set(kdenlive_SRCS ${kdenlive_SRCS} utils/KoIconUtils.cpp utils/abstractservice.cpp utils/archiveorg.cpp utils/devices.cpp utils/flowlayout.cpp utils/freesound.cpp utils/openclipart.cpp utils/resourcewidget.cpp utils/thememanager.cpp + utils/thumbnailcache.cpp PARENT_SCOPE ) diff --git a/src/utils/thumbnailcache.cpp b/src/utils/thumbnailcache.cpp new file mode 100644 index 000000000..72df69558 --- /dev/null +++ b/src/utils/thumbnailcache.cpp @@ -0,0 +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 "thumbnailcache.hpp" +#include "bin/projectclip.h" +#include "bin/projectitemmodel.h" +#include "core.h" +#include "doc/kdenlivedoc.h" +#include +#include +#include + +std::unique_ptr ThumbnailCache::instance; +std::once_flag ThumbnailCache::m_onceFlag; + +class ThumbnailCache::Cache_t +{ +public: + Cache_t(int maxCost) + : m_maxCost(maxCost) + { + } + + bool contains(const QString &key) const { return m_cache.count(key) > 0; } + + void remove(const QString &key) + { + if (!contains(key)) { + return; + } + auto it = m_cache.at(key); + m_currentCost -= (*it).second.second; + m_data.erase(it); + m_cache.erase(key); + } + + void insert(const QString &key, const QImage &img, int cost) + { + if (cost > m_maxCost) { + return; + } + m_data.push_front({key, {img, cost}}); + auto it = m_data.begin(); + m_cache[key] = it; + m_currentCost += cost; + while (m_currentCost > m_maxCost) { + remove(m_data.back().first); + } + } + + QImage get(const QString &key) + { + if (!contains(key)) { + return QImage(); + } + // when a get operation occurs, we put the corresponding list item in front to remember last access + std::pair> data; + auto it = m_cache.at(key); + std::swap(data, (*it)); // take data out without copy + QImage result = data.second.first; // a copy occurs here + m_data.erase(it); // delete old iterator + m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator + return result; + } + +protected: + int m_maxCost; + int m_currentCost{0}; + + std::list>> m_data; // the data is stored as (key,(image, cost)) + std::unordered_map m_cache; +}; + +ThumbnailCache::ThumbnailCache() + : m_volatileCache(new Cache_t(10000000)) +{ +} + +std::unique_ptr &ThumbnailCache::get() +{ + std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); }); + return instance; +} + +bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const +{ + QMutexLocker locker(&m_mutex); + auto key = getKey(binId, pos); + if (m_volatileCache->contains(key)) { + return true; + } + if (volatileOnly) { + return false; + } + bool ok = false; + QDir thumbFolder = getDir(&ok); + return ok && thumbFolder.exists(key); +} + +QImage ThumbnailCache::getThumbnail(const QString &binId, int pos, bool volatileOnly) const +{ + QMutexLocker locker(&m_mutex); + auto key = getKey(binId, pos); + if (m_volatileCache->contains(key)) { + return m_volatileCache->get(key); + } + if (volatileOnly) { + return QImage(); + } + bool ok = false; + QDir thumbFolder = getDir(&ok); + if (ok && thumbFolder.exists(key)) { + return QImage(thumbFolder.absoluteFilePath(key)); + } + return QImage(); +} + +void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent) +{ + QMutexLocker locker(&m_mutex); + auto key = getKey(binId, pos); + if (persistent) { + bool ok = false; + QDir thumbFolder = getDir(&ok); + if (ok) { + img.save(thumbFolder.absoluteFilePath(key)); + } + } else { + m_volatileCache->insert(key, img, img.byteCount()); + m_storedVolatile[binId].push_back(pos); + } +} + +void ThumbnailCache::invalidateThumbsForClip(const QString &binId) +{ + QMutexLocker locker(&m_mutex); + for (int pos : m_storedVolatile.at(binId)) { + auto key = getKey(binId, pos); + m_volatileCache->remove(key); + } + m_storedVolatile.erase(binId); + + Q_ASSERT(false); + // TODO implement the invalidation for persistent cache +} + +// static +QString ThumbnailCache::getKey(const QString &binId, int pos) +{ + auto binClip = pCore->projectItemModel()->getClipByBinID(binId); + return binClip->hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"); +} + +// static +QDir ThumbnailCache::getDir(bool *ok) +{ + return pCore->currentDoc()->getCacheDir(CacheThumbs, ok); +} diff --git a/src/utils/thumbnailcache.hpp b/src/utils/thumbnailcache.hpp new file mode 100644 index 000000000..61eadf7ed --- /dev/null +++ b/src/utils/thumbnailcache.hpp @@ -0,0 +1,93 @@ +/*************************************************************************** + * 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 "definitions.h" +#include +#include +#include +#include +#include +#include +#include + +/** @brief This class class is an interface to the caches that store thumbnails. + In Kdenlive, we use two such caches, a persistent that is stored on disk to allow thumbnails to be reused when reopening. + The other one is a volatile LRU cache that lives in memory. + Note that for the volatile cache uses a custom implementation. + QCache is not suitable since it operates on pointers and since the object is removed from the cache when accessed. + KImageCache is not suitable since it lacks a way to remove objects from the cache. + * Note that this class is a Singleton + */ + +class ThumbnailCache +{ + +public: + // Returns the instance of the Singleton + static std::unique_ptr &get(); + + /* @brief Check whether a given thumbnail is in the cache + @param binId is the id of the queried clip + @param pos is the position where we query + @param volatileOnly if true, we only check the volatile cache (no disk access) + */ + bool hasThumbnail(const QString &binId, int pos, bool volatileOnly = false) const; + + /* @brief Get a given thumbnail from the cache + @param binId is the id of the queried clip + @param pos is the position where we query + @param volatileOnly if true, we only check the volatile cache (no disk access) + */ + QImage getThumbnail(const QString &binId, int pos, bool volatileOnly = false) const; + + /* @brief Get a given thumbnail from the cache + @param binId is the id of the queried clip + @param pos is the position where we query + @param persistent if true, we store the image in the persistent cache, which generates a disk access + */ + void storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent = false); + + /* @brief Removes all the thumbnails for a given clip */ + void invalidateThumbsForClip(const QString &binId); + +protected: + // Constructor is protected because class is a Singleton + ThumbnailCache(); + + // Return the key associated to a thumbnail + static QString getKey(const QString &binId, int pos); + + // Return the dir where the persistent cache lives + static QDir getDir(bool *ok); + + static std::unique_ptr instance; + static std::once_flag m_onceFlag; // flag to create the repository only once; + + class Cache_t; + std::unique_ptr m_volatileCache; + mutable QMutex m_mutex; + + // the following maps keeps track of the positions that we store for each clip in volatile caches. + // Note that we don't track deletions due to items dropped from the cache. So the maps can contain more items that are currently stored. + std::unordered_map> m_storedVolatile; +}; diff --git a/src/xml/xml.cpp b/src/xml/xml.cpp index d99f0212d..57ae49eab 100644 --- a/src/xml/xml.cpp +++ b/src/xml/xml.cpp @@ -1,92 +1,109 @@ /*************************************************************************** * 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 "xml.hpp" #include // static QString Xml::getSubTagContent(const QDomElement &element, const QString &tagName) { QVector nodeList = getDirectChildrenByTagName(element, tagName); if (!nodeList.isEmpty()) { if (nodeList.size() > 1) { QString str; QTextStream stream(&str); element.save(stream, 4); qDebug() << "Warning: " << str << "provides several " << tagName << ". We keep only first one."; } QString content = nodeList.at(0).toElement().text(); return content; } return QString(); } QVector Xml::getDirectChildrenByTagName(const QDomElement &element, const QString &tagName) { auto children = element.childNodes(); QVector result; for (int i = 0; i < children.count(); ++i) { if (children.item(i).isNull() || !children.item(i).isElement()) { continue; } QDomElement child = children.item(i).toElement(); if (child.tagName() == tagName) { result.push_back(child); } } return result; } -QString Xml::getTagContentByAttribute(const QDomElement &element, const QString &tagName, const QString &attribute, const QString &value, const QString &defaultReturn, bool directChildren) +QString Xml::getTagContentByAttribute(const QDomElement &element, const QString &tagName, const QString &attribute, const QString &value, + const QString &defaultReturn, bool directChildren) { QDomNodeList nodes; if (directChildren) { nodes = element.childNodes(); } else { nodes = element.elementsByTagName(tagName); } for (int i = 0; i < nodes.count(); ++i) { auto current = nodes.item(i); if (current.isNull() || !current.isElement()) { continue; } auto elem = current.toElement(); if (elem.tagName() == tagName && elem.hasAttribute(attribute)) { if (elem.attribute(attribute) == value) { return elem.text(); } } } return defaultReturn; } -void Xml::addXmlProperties(QDomElement &element, QMap &properties) +void Xml::addXmlProperties(QDomElement &element, const std::unordered_map &properties) +{ + for (const auto &p : properties) { + QDomElement prop = element.ownerDocument().createElement(QStringLiteral("property")); + prop.setAttribute(QStringLiteral("name"), p.first); + QDomText value = element.ownerDocument().createTextNode(p.second); + prop.appendChild(value); + element.appendChild(prop); + } +} + +void Xml::addXmlProperties(QDomElement &element, const QMap &properties) { QMapIterator i(properties); while (i.hasNext()) { i.next(); QDomElement prop = element.ownerDocument().createElement(QStringLiteral("property")); prop.setAttribute(QStringLiteral("name"), i.key()); QDomText value = element.ownerDocument().createTextNode(i.value()); prop.appendChild(value); element.appendChild(prop); } } + +QString Xml::getXmlProperty(const QDomElement &element, const QString &propertyName, const QString &defaultReturn) +{ + return Xml::getTagContentByAttribute(element, QStringLiteral("property"), QStringLiteral("name"), propertyName, defaultReturn, false); +} diff --git a/src/xml/xml.hpp b/src/xml/xml.hpp index 7bc203a48..f04f1a40c 100644 --- a/src/xml/xml.hpp +++ b/src/xml/xml.hpp @@ -1,65 +1,72 @@ /*************************************************************************** * 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 XML_H #define XML_H +#include "definitions.h" #include #include #include +#include /** @brief This static class provides helper functions to manipulate Dom objects easily */ -namespace Xml -{ +namespace Xml { +/* @brief Returns the content of a given tag within the current DomElement. + For example, if your @param element looks like foobar, passing @tagName = "title" will return foo, and @tagName + = "head" returns bar + Returns empty string if tag is not found. +*/ +QString getSubTagContent(const QDomElement &element, const QString &tagName); - /* @brief Returns the content of a given tag within the current DomElement. - For example, if your @param element looks like foobar, passing @tagName = "title" will return foo, and @tagName - = "head" returns bar - Returns empty string if tag is not found. - */ - QString getSubTagContent(const QDomElement &element, const QString &tagName); +/* @brief Returns the direct children of given @element whose tag name matches given @param tagName + This is an alternative to QDomElement::elementsByTagName which returns also non-direct children +*/ +QVector getDirectChildrenByTagName(const QDomElement &element, const QString &tagName); - /* @brief Returns the direct children of given @element whose tag name matches given @param tagName - This is an alternative to QDomElement::elementsByTagName which returns also non-direct children - */ - QVector getDirectChildrenByTagName(const QDomElement &element, const QString &tagName); +/* @brief Returns the content of a children tag of @param element, which respects the following conditions : + - Its type is @param tagName + - It as an attribute named @param attribute with value @param value + For example, if your element is bar, you can retrieve "bar" with parameters: tagName="param", attribute="val", and + value="foo" Returns @param defaultReturn when nothing is found. The methods returns the first match found, so make sure there can't be more than one. If + @param directChildren is true, only immediate children of the node are considered +*/ +QString getTagContentByAttribute(const QDomElement &element, const QString &tagName, const QString &attribute, const QString &value, + const QString &defaultReturn = QString(), bool directChildren = true); - /* @brief Returns the content of a children tag of @param element, which respects the following conditions : - - Its type is @param tagName - - It as an attribute named @param attribute with value @param value - For example, if your element is bar, you can retrieve "bar" with parameters: tagName="param", attribute="val", and value="foo" - Returns @param defaultReturn when nothing is found. The methods returns the first match found, so make sure there can't be more than one. - If @param directChildren is true, only immediate children of the node are considered - */ - QString getTagContentByAttribute(const QDomElement &element, const QString &tagName, const QString &attribute, const QString &value, const QString &defaultReturn = QString(), bool directChildren = true); +/* @brief This is a specialization of getTagContentByAttribute with tagName = "property" and attribute = "name". + That is, to match something like bar, pass propertyName = foo, and this will return bar +*/ +QString getXmlProperty(const QDomElement &element, const QString &propertyName, const QString &defaultReturn = QString()); - /* @brief Add properties to the given xml element - For each element (n, v) in the properties map, it creates a sub element of the form : v - @param producer is the xml element where to append properties - @param properties is the map of properties - */ - void addXmlProperties(QDomElement &producer, QMap &properties); -} +/* @brief Add properties to the given xml element + For each element (n, v) in the properties map, it creates a sub element of the form : v + @param producer is the xml element where to append properties + @param properties is the map of properties + */ +void addXmlProperties(QDomElement &producer, const std::unordered_map &properties); +void addXmlProperties(QDomElement &producer, const QMap &properties); +} // namespace Xml #endif