diff --git a/src/core.cpp b/src/core.cpp index 63353d1f1..5cdf6478f 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,742 +1,714 @@ /* 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 "capture/mediacapture.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/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.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_thumbProfile(nullptr) , m_capture(new MediaCapture(this)) { } void Core::prepareShutdown() { m_guiConstructed = false; } Core::~Core() { if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(const QString &MltPath) { if (m_self) { 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(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } 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 = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); m_monitorManager = new MonitorManager(this); // 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::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); 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)));*/ m_mainWindow->init(); projectManager()->init(Url, QString()); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); 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(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { 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; m_thumbProfile.reset(); // inform render widget m_mainWindow->updateRenderWidgetProfile(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } 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 {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: 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)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } break; case ObjectType::TimelineComposition: return 0; break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: 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)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(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) { 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 || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: 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)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->refreshClipMonitor(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } 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) { if (m_mainWindow) { m_mainWindow->displayMessage(message, type, timeout); } else { qDebug() << message; } } void Core::displayBinMessage(const QString &text, int type, const QList &actions) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId); 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(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(true); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(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) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_mainWindow && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade")) && id.first == ObjectType::TimelineClip) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } 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); } } Mlt::Profile *Core::thumbProfile() { if (!m_thumbProfile) { m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_thumbProfile->set_height(200); int width = 200 * m_thumbProfile->dar(); if (width % 8 > 0) { width += 8 - width % 8; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } -void Core::clearSelection() -{ - if (m_mainWindow && m_guiConstructed) { - m_mainWindow->getCurrentTimeline()->controller()->clearSelection(); - } -} - -void Core::selectItem(int itemId) -{ - if (m_mainWindow && m_guiConstructed) { - m_mainWindow->getCurrentTimeline()->controller()->addSelection(itemId, true); - } -} - -bool Core::isSelected(int itemId) const -{ - if (m_mainWindow && m_guiConstructed) { - return m_mainWindow->getCurrentTimeline()->controller()->selection().contains(itemId); - } - return false; -} - -void Core::removeFromSelection(int itemId) -{ - if (m_mainWindow && m_guiConstructed) { - m_mainWindow->getCurrentTimeline()->controller()->removeSelection(itemId); - } -} void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } void Core::startMediaCapture(bool checkAudio, bool checkVideo, QUrl path, QString audioDevice) { m_capture->setCaptureOutputLocation(path); m_capture->setAudioCaptureDevice(audioDevice); if (checkAudio && checkVideo) { m_capture->recordVideo(true); } else if (checkAudio) { m_capture->recordAudio(true); } } void Core::stopMediaCapture(bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(false); } else if (checkAudio && !checkVideo) { m_capture->recordAudio(false); } } QStringList Core::getAudioCaptureDevices() { return m_capture->getAudioCaptureDevices(); } int Core::getMediaCaptureState() { return m_capture->getState(); } void Core::setAudioCaptureVolume(int volume) { m_capture->setAudioVolume(volume / 100.0); } MediaCapture *Core::getAudioDevice() { return m_capture.get(); } diff --git a/src/core.h b/src/core.h index 653af1983..6dbe4a5e0 100644 --- a/src/core.h +++ b/src/core.h @@ -1,228 +1,224 @@ /* 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 DocUndoStack; class EffectStackModel; class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class MediaCapture; class Monitor; class MonitorManager; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; class Profile; } // namespace Mlt #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; ~Core() override; /** * @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, otherwise, 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 Delete the global core instance */ static void clean(); /** @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 view of the project bin. */ Bin *bin(); /** @brief Select a clip in the Bin from its id. */ void selectBinClip(const QString &id, int frame = -1, const QPoint &zone = QPoint()); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); /** @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); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemPosition(const ObjectId &id); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Returns the capabilities of a clip: AudioOnly, VideoOnly or Disabled if both are allowed */ PlaylistState::ClipState getItemState(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) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; std::shared_ptr undoStack(); double getClipSpeed(int id) const; /** @brief Mark an item as invalid for timeline preview */ void invalidateItem(ObjectId itemId); void invalidateRange(QSize range); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** A fade for clip id changed, update timeline */ void updateItemModel(ObjectId id, const QString &service); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); Mlt::Profile *thumbProfile(); - void clearSelection(); - void selectItem(int itemId); - bool isSelected(int itemId) const; - void removeFromSelection(int itemId); /** @brief Returns the current project duration */ int projectDuration() const; /** @brief Returns true if current project has some rendered timeline preview */ bool hasTimelinePreview() const; /** @brief Handles audio and video capture **/ void startMediaCapture(bool, bool, QUrl, QString); void stopMediaCapture(bool, bool); QStringList getAudioCaptureDevices(); int getMediaCaptureState(); void setAudioCaptureVolume(int); MediaCapture *getAudioDevice(); 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{nullptr}; ProjectManager *m_projectManager{nullptr}; MonitorManager *m_monitorManager{nullptr}; std::shared_ptr m_projectItemModel; std::shared_ptr m_jobManager; Bin *m_binWidget{nullptr}; LibraryWidget *m_library{nullptr}; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; std::unique_ptr m_thumbProfile; bool m_guiConstructed = false; /** @brief Check that the profile is valid (width is a multiple of 8 and height a multiple of 2 */ void checkProfileValidity(); std::unique_ptr m_capture; public slots: void triggerAction(const QString &name); /** @brief display a user info/warning message in the project bin */ void displayBinMessage(const QString &text, int type, const QList &actions = QList()); void displayBinLogMessage(const QString &text, int type, const QString &logInfo); signals: void coreIsReady(); void updateLibraryPath(); }; #endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9f9b18b44..4d0a324c4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,3798 +1,3798 @@ /*************************************************************************** * 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 "hidetitlebars.h" #include "jobs/jobmanager.h" #include "jobs/scenesplitjob.hpp" #include "jobs/speedjob.hpp" #include "jobs/stabilizejob.hpp" #include "kdenlivesettings.h" #include "layoutmanagement.h" #include "library/librarywidget.h" #include "mainwindowadaptor.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "monitor/scopes/audiographspectrum.h" #include "profiles/profilemodel.hpp" #include "project/cliptranscode.h" #include "project/dialogs/archivewidget.h" #include "project/dialogs/projectsettings.h" #include "project/projectcommands.h" #include "project/projectmanager.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 "profiles/profilerepository.hpp" #include "widgets/progressbutton.h" #include #include "project/dialogs/temporarydata.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 static const char version[] = KDENLIVE_VERSION; namespace Mlt { class Producer; } 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) { } void MainWindow::init() { QString desktopStyle = QApplication::style()->objectName(); // Load themes auto themeManager = new ThemeManager(actionCollection()); actionCollection()->addAction(QStringLiteral("themes_menu"), themeManager); connect(themeManager, &ThemeManager::themeChanged, this, &MainWindow::slotThemeChanged); if (!KdenliveSettings::widgetstyle().isEmpty() && QString::compare(desktopStyle, KdenliveSettings::widgetstyle(), Qt::CaseInsensitive) != 0) { // User wants a custom widget style, init doChangeStyle(); } // 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(); if (KdenliveSettings::widgetstyle().isEmpty()) { // First run QStringList 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")); } } else { KdenliveSettings::setWidgetstyle(QStringLiteral("Default")); } } // Add default style action QAction *defaultStyle = new QAction(i18n("Default"), stylesGroup); defaultStyle->setData(QStringLiteral("Default")); defaultStyle->setCheckable(true); stylesAction->addAction(defaultStyle); if (KdenliveSettings::widgetstyle() == QLatin1String("Default") || 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 = EffectsRepository::get()->hasInternalEffect(QStringLiteral("glsl.manager")); m_shortcutRemoveFocus = new QShortcut(QKeySequence(QStringLiteral("Esc")), this); connect(m_shortcutRemoveFocus, &QShortcut::activated, this, &MainWindow::slotRemoveFocus); /// Add Widgets setDockOptions(dockOptions() | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); setDockOptions(dockOptions() | QMainWindow::GroupedDragging); 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)));*/ // 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::slotDeleteGuide); 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); // Audio spectrum scope m_audioSpectrum = new AudioGraphSpectrum(pCore->monitorManager()); QDockWidget *spectrumDock = addDock(i18n("Audio Spectrum"), QStringLiteral("audiospectrum"), m_audioSpectrum); // 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); m_effectStackDock = addDock(i18n("Properties"), QStringLiteral("effect_stack"), m_assetPanel); 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::showTransitionModel, [&] () { m_effectStackDock->raise(); }); connect(m_timelineTabs, &TimelineTabs::showItemEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(m_timelineTabs, &TimelineTabs::showItemEffectStack, [&] () { m_effectStackDock->raise(); }); connect(m_timelineTabs, &TimelineTabs::updateZoom, this, &MainWindow::updateZoomSlider); connect(pCore->bin(), &Bin::requestShowEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(pCore->bin(), &Bin::requestShowEffectStack, [&] () { m_effectStackDock->raise(); }); connect(this, &MainWindow::clearAssetPanel, m_assetPanel, &AssetPanel::clearAssetPanel); connect(m_assetPanel, &AssetPanel::seekToPos, [this](int pos) { ObjectId oId = m_assetPanel->effectStackOwner(); switch (oId.first) { case ObjectType::TimelineTrack: case ObjectType::TimelineClip: case ObjectType::TimelineComposition: getCurrentTimeline()->controller()->setPosition(pos); break; case ObjectType::BinClip: m_clipMonitor->requestSeek(pos); break; default: qDebug() << "ERROR unhandled object type"; break; } }); m_effectList2 = new EffectListWidget(this); connect(m_effectList2, &EffectListWidget::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); connect(m_assetPanel, &AssetPanel::reloadEffect, m_effectList2, &EffectListWidget::reloadCustomEffect); m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList2); m_transitionList2 = new TransitionListWidget(this); 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(QIcon::fromTheme(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 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_effectList2->reloadEffectMenu(m_effectsMenu, m_effectActions); m_transitionsMenu = new QMenu(i18n("Add Transition"), this); m_transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); 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(this); connect(m_effectBasket, &EffectBasket::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); connect(m_effectList2, &EffectListWidget::reloadFavorites, m_effectBasket, &EffectBasket::slotReloadBasket); auto *widgetlist = new QWidgetAction(this); widgetlist->setDefaultWidget(m_effectBasket); // widgetlist->setText(i18n("Favorite Effects")); widgetlist->setToolTip(i18n("Favorite Effects")); widgetlist->setIcon(QIcon::fromTheme(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(QIcon::fromTheme(QStringLiteral("favorite"))); auto *toolButtonAction = new QWidgetAction(this); toolButtonAction->setText(i18n("Favorite Effects")); toolButtonAction->setIcon(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(QStringLiteral("preview-render-on"))); previewButtonAction->setDefaultWidget(timelinePreview); addAction(QStringLiteral("timeline_preview_button"), previewButtonAction); setupGUI(KXmlGuiWindow::ToolBar | KXmlGuiWindow::StatusBar | KXmlGuiWindow::Save | KXmlGuiWindow::Create); if (firstRun) { if (QScreen *current = QApplication::primaryScreen()) { 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(QIcon::fromTheme(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::slotAddEffect); connect(m_effectsMenu, &QMenu::triggered, this, &MainWindow::slotAddEffect); connect(m_transitionsMenu, &QMenu::triggered, this, &MainWindow::slotAddTransition); m_timelineContextMenu = 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))); // QMenu *markersMenu = static_cast(factory()->container(QStringLiteral("marker_menu"), this)); /*m_timelineClipActions->addMenu(markersMenu); m_timelineClipActions->addSeparator(); m_timelineClipActions->addMenu(m_transitionsMenu); m_timelineClipActions->addMenu(m_effectsMenu);*/ 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(QIcon::fromTheme(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); /*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); #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 &name) { KSharedConfigPtr config = KSharedConfig::openConfig(name); QPalette plt = KColorScheme::createApplicationPalette(config); // qApp->setPalette(plt); // Required for qml palette change QGuiApplication::setPalette(plt); QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (m_assetPanel) { m_assetPanel->updatePalette(); } if (m_effectList2) { // Trigger a repaint to have icons adapted m_effectList2->reset(); } if (m_transitionList2) { // Trigger a repaint to have icons adapted m_transitionList2->reset(); } if (m_clipMonitor) { m_clipMonitor->setPalette(plt); } if (m_projectMonitor) { m_projectMonitor->setPalette(plt); } if (m_timelineTabs) { m_timelineTabs->setPalette(plt); getMainTimeline()->controller()->resetView(); } if (m_audioSpectrum) { m_audioSpectrum->refreshPixmap(); } KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup initialGroup(kconfig, "version"); if (initialGroup.exists() && KdenliveSettings::force_breeze() && useDarkIcons != KdenliveSettings::use_dark_breeze()) { // We need to reload icon theme QIcon::setThemeName(useDarkIcons ? QStringLiteral("breeze-dark") : QStringLiteral("breeze")); KdenliveSettings::setUse_dark_breeze(useDarkIcons); } #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(); } for (QAction *action : actionCollection()->actions()) { QIcon icon = action->icon(); if (icon.isNull()) { continue; } QString iconName = icon.name(); QIcon newIcon = QIcon::fromTheme(iconName); if (newIcon.isNull()) { continue; } action->setIcon(newIcon); } } m_themeInitialized = true; m_isDarkTheme = useDarkIcons; #endif } 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() { auto *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())); } MainWindow::~MainWindow() { pCore->prepareShutdown(); m_timelineTabs->disconnectTimeline(getMainTimeline()); 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")))) { 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(const QStringList &paths) { for (const QString &p : paths) { EffectsRepository::get()->reloadCustom(p); } m_effectList2->reloadEffectMenu(m_effectsMenu, m_effectActions); } void MainWindow::configureNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::slotFullScreen() { KToggleFullScreenAction::setFullScreen(this, actionCollection()->action(QStringLiteral("fullscreen"))->isChecked()); } 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, Qt::DirectConnection); connect(m_projectMonitor, &Monitor::removeSplitOverlay, this, &MainWindow::removeSplitOverlay, Qt::DirectConnection); } 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, const QKeySequence &shortcut, KActionCategory *category) { m_actionNames.append(name); if (category) { category->addAction(name, action); } else { actionCollection()->addAction(name, action); } actionCollection()->setDefaultShortcut(action, shortcut); } QAction *MainWindow::addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon, const QKeySequence &shortcut, KActionCategory *category) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } addAction(name, action, shortcut, category); connect(action, SIGNAL(triggered(bool)), receiver, member); return action; } void MainWindow::setupActions() { // create edit mode buttons m_normalEditTool = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-normal-edit")), i18n("Normal mode"), this); m_normalEditTool->setCheckable(true); m_normalEditTool->setChecked(true); m_overwriteEditTool = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-overwrite-edit")), i18n("Overwrite mode"), this); m_overwriteEditTool->setCheckable(true); m_overwriteEditTool->setChecked(false); m_insertEditTool = new QAction(QIcon::fromTheme(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, static_cast(&KSelectAction::triggered), this, &MainWindow::slotChangeEdit); addAction(QStringLiteral("timeline_mode"), sceneMode); m_useTimelineZone = new KDualAction(i18n("Do not Use Timeline Zone for Insert"), i18n("Use Timeline Zone for Insert"), this); m_useTimelineZone->setActiveIcon(QIcon::fromTheme(QStringLiteral("timeline-use-zone-on"))); m_useTimelineZone->setInactiveIcon(QIcon::fromTheme(QStringLiteral("timeline-use-zone-off"))); m_useTimelineZone->setAutoToggle(true); connect(m_useTimelineZone, &KDualAction::activeChangedByUser, this, &MainWindow::slotSwitchTimelineZone); addAction(QStringLiteral("use_timeline_zone_in_edit"), m_useTimelineZone, Qt::Key_G); m_compositeAction = new KSelectAction(QIcon::fromTheme(QStringLiteral("composite-track-off")), i18n("Track compositing"), this); m_compositeAction->setToolTip(i18n("Track compositing")); QAction *noComposite = new QAction(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(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, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateCompositing); addAction(QStringLiteral("timeline_compositing"), m_compositeAction); QAction *splitView = new QAction(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")), i18n("Split Audio Tracks"), this); addAction(QStringLiteral("timeline_view_split"), splitView); splitView->setData(QVariant::fromValue(1)); splitView->setCheckable(true); splitView->setChecked(KdenliveSettings::audiotracksbelow()); QAction *mixedView = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Mixed Audio tracks"), this); addAction(QStringLiteral("timeline_mixed_view"), mixedView); mixedView->setData(QVariant::fromValue(0)); mixedView->setCheckable(true); mixedView->setChecked(!KdenliveSettings::audiotracksbelow()); auto *clipTypeGroup = new QActionGroup(this); clipTypeGroup->addAction(mixedView); clipTypeGroup->addAction(splitView); connect(clipTypeGroup, &QActionGroup::triggered, this, &MainWindow::slotUpdateTimelineView); auto tlsettings = new QMenu(this); tlsettings->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); tlsettings->addAction(m_compositeAction); tlsettings->addAction(mixedView); tlsettings->addAction(splitView); addAction(QStringLiteral("timeline_settings"), tlsettings->menuAction()); 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, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateTimecodeFormat); m_timeFormatButton->setToolBarMode(KSelectAction::MenuMode); m_timeFormatButton->setToolButtonPopupMode(QToolButton::InstantPopup); addAction(QStringLiteral("timeline_timecode"), m_timeFormatButton); // create tools buttons m_buttonSelectTool = new QAction(QIcon::fromTheme(QStringLiteral("cursor-arrow")), i18n("Selection tool"), this); // toolbar->addAction(m_buttonSelectTool); m_buttonSelectTool->setCheckable(true); m_buttonSelectTool->setChecked(true); m_buttonRazorTool = new QAction(QIcon::fromTheme(QStringLiteral("edit-cut")), i18n("Razor tool"), this); // toolbar->addAction(m_buttonRazorTool); m_buttonRazorTool->setCheckable(true); m_buttonRazorTool->setChecked(false); m_buttonSpacerTool = new QAction(QIcon::fromTheme(QStringLiteral("distribute-horizontal-x")), i18n("Spacer tool"), this); // 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); m_buttonVideoThumbs = new QAction(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(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(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit zoom to project"), this); m_buttonFitZoom->setCheckable(false); m_zoomSlider = new QSlider(Qt::Horizontal, this); m_zoomSlider->setRange(0, 20); m_zoomSlider->setPageStep(1); m_zoomSlider->setInvertedAppearance(true); m_zoomSlider->setInvertedControls(true); m_zoomSlider->setMaximumWidth(150); m_zoomSlider->setMinimumWidth(100); m_zoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), actionCollection()); m_zoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), actionCollection()); 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); 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_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); 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, Qt::Key_S); addAction(QStringLiteral("razor_tool"), m_buttonRazorTool, Qt::Key_X); addAction(QStringLiteral("spacer_tool"), m_buttonSpacerTool, Qt::Key_M); 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("run_wizard"), i18n("Run Config Wizard"), this, SLOT(slotRunWizard()), QIcon::fromTheme(QStringLiteral("tools-wizard"))); addAction(QStringLiteral("project_settings"), i18n("Project Settings"), this, SLOT(slotEditProjectSettings()), QIcon::fromTheme(QStringLiteral("configure"))); addAction(QStringLiteral("project_render"), i18n("Render"), this, SLOT(slotRenderProject()), QIcon::fromTheme(QStringLiteral("media-record")), Qt::CTRL + Qt::Key_Return); addAction(QStringLiteral("stop_project_render"), i18n("Stop Render"), this, SLOT(slotStopRenderProject()), QIcon::fromTheme(QStringLiteral("media-record"))); addAction(QStringLiteral("project_clean"), i18n("Clean Project"), this, SLOT(slotCleanProject()), QIcon::fromTheme(QStringLiteral("edit-clear"))); 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()), QIcon::fromTheme(QStringLiteral("media-playback-start")), Qt::CTRL + Qt::Key_Space); m_loopZone = addAction(QStringLiteral("monitor_loop_zone"), i18n("Loop Zone"), pCore->monitorManager(), SLOT(slotLoopZone()), QIcon::fromTheme(QStringLiteral("media-playback-start")), Qt::ALT + Qt::Key_Space); m_loopClip = new QAction(QIcon::fromTheme(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()), QIcon::fromTheme(QStringLiteral("media-optical"))); addAction(QStringLiteral("transcode_clip"), i18n("Transcode Clips"), this, SLOT(slotTranscodeClip()), QIcon::fromTheme(QStringLiteral("edit-copy"))); addAction(QStringLiteral("archive_project"), i18n("Archive Project"), this, SLOT(slotArchiveProject()), QIcon::fromTheme(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()), QIcon::fromTheme(QStringLiteral("document-open"))); QAction *overlayInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Info Overlay"), this); addAction(QStringLiteral("monitor_overlay"), overlayInfo); overlayInfo->setCheckable(true); overlayInfo->setData(0x01); QAction *overlayTCInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Timecode"), this); addAction(QStringLiteral("monitor_overlay_tc"), overlayTCInfo); overlayTCInfo->setCheckable(true); overlayTCInfo->setData(0x02); QAction *overlayFpsInfo = new QAction(QIcon::fromTheme(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(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Markers"), this); addAction(QStringLiteral("monitor_overlay_markers"), overlayMarkerInfo); overlayMarkerInfo->setCheckable(true); overlayMarkerInfo->setData(0x04); QAction *overlayAudioInfo = new QAction(QIcon::fromTheme(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()); addAction(QStringLiteral("mlt_realtime"), 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, static_cast(&KSelectAction::triggered), this, &MainWindow::slotSetMonitorGamma); addAction(QStringLiteral("switch_trim"), i18n("Trim Mode"), this, SLOT(slotSwitchTrimMode()), QIcon::fromTheme(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::fromTheme(QStringLiteral("kdenlive-add-clip")), Qt::CTRL + Qt::Key_I); addAction(QStringLiteral("monitor_seek_snap_backward"), i18n("Go to Previous Snap Point"), this, SLOT(slotSnapRewind()), QIcon::fromTheme(QStringLiteral("media-seek-backward")), Qt::ALT + Qt::Key_Left); addAction(QStringLiteral("seek_clip_start"), i18n("Go to Clip Start"), this, SLOT(slotClipStart()), QIcon::fromTheme(QStringLiteral("media-seek-backward")), Qt::Key_Home); addAction(QStringLiteral("seek_clip_end"), i18n("Go to Clip End"), this, SLOT(slotClipEnd()), QIcon::fromTheme(QStringLiteral("media-seek-forward")), Qt::Key_End); addAction(QStringLiteral("monitor_seek_snap_forward"), i18n("Go to Next Snap Point"), this, SLOT(slotSnapForward()), QIcon::fromTheme(QStringLiteral("media-seek-forward")), Qt::ALT + Qt::Key_Right); addAction(QStringLiteral("align_playhead"), i18n("Align Playhead to Mouse Position"), this, SLOT(slotAlignPlayheadToMousePos()), QIcon(), Qt::Key_P); addAction(QStringLiteral("grab_item"), i18n("Grab Current Item"), this, SLOT(slotGrabItem()), QIcon::fromTheme(QStringLiteral("transform-move")), Qt::SHIFT + Qt::Key_G); 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("overwrite_to_in_point"), i18n("Overwrite Clip Zone in Timeline"), this, SLOT(slotInsertClipOverwrite()), QIcon::fromTheme(QStringLiteral("timeline-overwrite")), Qt::Key_B); addAction(QStringLiteral("insert_to_in_point"), i18n("Insert Clip Zone in Timeline"), this, SLOT(slotInsertClipInsert()), QIcon::fromTheme(QStringLiteral("timeline-insert")), Qt::Key_V); addAction(QStringLiteral("remove_extract"), i18n("Extract Timeline Zone"), this, SLOT(slotExtractZone()), QIcon::fromTheme(QStringLiteral("timeline-extract")), Qt::SHIFT + Qt::Key_X); addAction(QStringLiteral("remove_lift"), i18n("Lift Timeline Zone"), this, SLOT(slotLiftZone()), QIcon::fromTheme(QStringLiteral("timeline-lift")), Qt::Key_Z); addAction(QStringLiteral("set_render_timeline_zone"), i18n("Add Preview Zone"), this, SLOT(slotDefinePreviewRender()), QIcon::fromTheme(QStringLiteral("preview-add-zone"))); addAction(QStringLiteral("unset_render_timeline_zone"), i18n("Remove Preview Zone"), this, SLOT(slotRemovePreviewRender()), QIcon::fromTheme(QStringLiteral("preview-remove-zone"))); addAction(QStringLiteral("clear_render_timeline_zone"), i18n("Remove All Preview Zones"), this, SLOT(slotClearPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-remove-all"))); addAction(QStringLiteral("prerender_timeline_zone"), i18n("Start Preview Render"), this, SLOT(slotPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-render-on")), QKeySequence(Qt::SHIFT + Qt::Key_Return)); addAction(QStringLiteral("stop_prerender_timeline"), i18n("Stop Preview Render"), this, SLOT(slotStopPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-render-off"))); addAction(QStringLiteral("select_timeline_clip"), i18n("Select Clip"), this, SLOT(slotSelectTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_clip"), i18n("Deselect Clip"), this, SLOT(slotDeselectTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_clip"), i18n("Add Clip To Selection"), this, SLOT(slotSelectAddTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::ALT + Qt::Key_Plus); addAction(QStringLiteral("select_timeline_transition"), i18n("Select Transition"), this, SLOT(slotSelectTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_transition"), i18n("Deselect Transition"), this, SLOT(slotDeselectTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_transition"), i18n("Add Transition To Selection"), this, SLOT(slotSelectAddTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::ALT + Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("add_clip_marker"), i18n("Add Marker"), this, SLOT(slotAddClipMarker()), QIcon::fromTheme(QStringLiteral("bookmark-new"))); addAction(QStringLiteral("delete_clip_marker"), i18n("Delete Marker"), this, SLOT(slotDeleteClipMarker()), QIcon::fromTheme(QStringLiteral("edit-delete"))); addAction(QStringLiteral("delete_all_clip_markers"), i18n("Delete All Markers"), this, SLOT(slotDeleteAllClipMarkers()), QIcon::fromTheme(QStringLiteral("edit-delete"))); QAction *editClipMarker = addAction(QStringLiteral("edit_clip_marker"), i18n("Edit Marker"), this, SLOT(slotEditClipMarker()), QIcon::fromTheme(QStringLiteral("document-properties"))); editClipMarker->setData(QStringLiteral("edit_marker")); addAction(QStringLiteral("add_marker_guide_quickly"), i18n("Add Marker/Guide quickly"), this, SLOT(slotAddMarkerGuideQuickly()), QIcon::fromTheme(QStringLiteral("bookmark-new")), Qt::Key_Asterisk); // Clip actions. We set some category info on the action data to enable/disable it contextually in timelinecontroller KActionCategory *clipActionCategory = new KActionCategory(i18n("Current Selection"), actionCollection()); QAction *splitAudio = addAction(QStringLiteral("clip_split"), i18n("Split Audio"), this, SLOT(slotSplitAV()), QIcon::fromTheme(QStringLiteral("document-new")), QKeySequence(), clipActionCategory); // "S" will be handled specifically to change the action name depending on current selection splitAudio->setData('S'); splitAudio->setEnabled(false); QAction *setAudioAlignReference = addAction(QStringLiteral("set_audio_align_ref"), i18n("Set Audio Reference"), this, SLOT(slotSetAudioAlignReference()), QIcon(), QKeySequence(), clipActionCategory); // "A" as data means this action should only be available for clips with audio setAudioAlignReference->setData('A'); setAudioAlignReference->setEnabled(false); QAction *alignAudio = addAction(QStringLiteral("align_audio"), i18n("Align Audio to Reference"), this, SLOT(slotAlignAudio()), QIcon(), QKeySequence(), clipActionCategory); // "A" as data means this action should only be available for clips with audio alignAudio->setData('A'); alignAudio->setEnabled(false); QAction *act = addAction(QStringLiteral("edit_item_duration"), i18n("Edit Duration"), this, SLOT(slotEditItemDuration()), QIcon::fromTheme(QStringLiteral("measure")), QKeySequence(), clipActionCategory); act->setEnabled(false); act = addAction(QStringLiteral("clip_in_project_tree"), i18n("Clip in Project Bin"), this, SLOT(slotClipInProjectTree()), QIcon::fromTheme(QStringLiteral("go-jump-definition")), QKeySequence(), clipActionCategory); act->setEnabled(false); // "C" as data means this action should only be available for clips - not for compositions act->setData('C'); act = addAction(QStringLiteral("cut_timeline_clip"), i18n("Cut Clip"), this, SLOT(slotCutTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-cut")), Qt::SHIFT + Qt::Key_R); act = addAction(QStringLiteral("delete_timeline_clip"), i18n("Delete Selected Item"), this, SLOT(slotDeleteItem()), QIcon::fromTheme(QStringLiteral("edit-delete")), Qt::Key_Delete); QAction *resizeStart = new QAction(QIcon(), i18n("Resize Item Start"), this); addAction(QStringLiteral("resize_timeline_clip_start"), resizeStart, Qt::Key_1, clipActionCategory); resizeStart->setEnabled(false); connect(resizeStart, &QAction::triggered, this, &MainWindow::slotResizeItemStart); QAction *resizeEnd = new QAction(QIcon(), i18n("Resize Item End"), this); addAction(QStringLiteral("resize_timeline_clip_end"), resizeEnd, Qt::Key_2, clipActionCategory); resizeEnd->setEnabled(false); connect(resizeEnd, &QAction::triggered, this, &MainWindow::slotResizeItemEnd); QAction *pasteEffects = addAction(QStringLiteral("paste_effects"), i18n("Paste Effects"), this, SLOT(slotPasteEffects()), QIcon::fromTheme(QStringLiteral("edit-paste")), QKeySequence(), clipActionCategory); pasteEffects->setEnabled(false); // "C" as data means this action should only be available for clips - not for compositions pasteEffects->setData('C'); QAction *groupClip = addAction(QStringLiteral("group_clip"), i18n("Group Clips"), this, SLOT(slotGroupClips()), QIcon::fromTheme(QStringLiteral("object-group")), Qt::CTRL + Qt::Key_G, clipActionCategory); // "G" as data means this action should only be available for multiple items selection groupClip->setData('G'); groupClip->setEnabled(false); QAction *ungroupClip = addAction(QStringLiteral("ungroup_clip"), i18n("Ungroup Clips"), this, SLOT(slotUnGroupClips()), QIcon::fromTheme(QStringLiteral("object-ungroup")), Qt::CTRL + Qt::SHIFT + Qt::Key_G, clipActionCategory); // "U" as data means this action should only be available if selection is a group ungroupClip->setData('U'); ungroupClip->setEnabled(false); act = clipActionCategory->addAction(KStandardAction::Copy, this, SLOT(slotCopy())); act->setEnabled(false); KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); /*act = KStandardAction::copy(this, SLOT(slotCopy()), actionCollection()); clipActionCategory->addAction(KStandardAction::name(KStandardAction::Copy), act); act->setEnabled(false); act = KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); clipActionCategory->addAction(KStandardAction::name(KStandardAction::Paste), act); act->setEnabled(false);*/ kdenliveCategoryMap.insert(QStringLiteral("timelineselection"), clipActionCategory); 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 *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(QIcon::fromTheme(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(QIcon::fromTheme(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()), QIcon::fromTheme(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()), QIcon::fromTheme(QStringLiteral("list-add"))); addAction(QStringLiteral("delete_guide"), i18n("Delete Guide"), this, SLOT(slotDeleteGuide()), QIcon::fromTheme(QStringLiteral("edit-delete"))); addAction(QStringLiteral("edit_guide"), i18n("Edit Guide"), this, SLOT(slotEditGuide()), QIcon::fromTheme(QStringLiteral("document-properties"))); addAction(QStringLiteral("delete_all_guides"), i18n("Delete All Guides"), this, SLOT(slotDeleteAllGuides()), QIcon::fromTheme(QStringLiteral("edit-delete"))); m_saveAction = KStandardAction::save(pCore->projectManager(), SLOT(saveFile()), actionCollection()); m_saveAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); addAction(QStringLiteral("save_selection"), i18n("Save Selection"), pCore->projectManager(), SLOT(slotSaveSelection()), QIcon::fromTheme(QStringLiteral("document-save"))); QAction *sentToLibrary = addAction(QStringLiteral("send_library"), i18n("Add Timeline Selection to Library"), pCore->library(), SLOT(slotAddToLibrary()), QIcon::fromTheme(QStringLiteral("bookmark-new"))); sentToLibrary->setEnabled(false); pCore->library()->setupActions(QList() << sentToLibrary); KStandardAction::showMenubar(this, SLOT(showMenuBar(bool)), actionCollection()); act = KStandardAction::quit(this, SLOT(close()), actionCollection()); // act->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); KStandardAction::keyBindings(this, SLOT(slotEditKeys()), actionCollection()); KStandardAction::preferences(this, SLOT(slotPreferences()), actionCollection()); KStandardAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection()); KStandardAction::fullScreen(this, SLOT(slotFullScreen()), this, actionCollection()); QAction *undo = KStandardAction::undo(m_commandStack, SLOT(undo()), actionCollection()); undo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canUndoChanged, undo, &QAction::setEnabled); QAction *redo = KStandardAction::redo(m_commandStack, SLOT(redo()), actionCollection()); 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()), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip"))); addClips->addAction(addClip); QAction *action = addAction(QStringLiteral("add_color_clip"), i18n("Add Color Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip"))); action->setData((int)ClipType::Color); addClips->addAction(action); action = addAction(QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip"))); action->setData((int)ClipType::SlideShow); addClips->addAction(action); action = addAction(QStringLiteral("add_text_clip"), i18n("Add Title Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::Text); addClips->addAction(action); action = addAction(QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::TextTemplate); addClips->addAction(action); /*action = addAction(QStringLiteral("add_qtext_clip"), i18n("Add Simple Text Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(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()), QIcon::fromTheme(QStringLiteral("folder-new"))); addClips->addAction(addAction(QStringLiteral("download_resource"), i18n("Online Resources"), this, SLOT(slotDownloadResources()), QIcon::fromTheme(QStringLiteral("edit-download")))); QAction *clipProperties = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), pCore->bin(), SLOT(slotSwitchClipProperties()), QIcon::fromTheme(QStringLiteral("document-edit"))); clipProperties->setData("clip_properties"); QAction *openClip = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), pCore->bin(), SLOT(slotOpenClip()), QIcon::fromTheme(QStringLiteral("document-open"))); openClip->setData("edit_clip"); openClip->setEnabled(false); QAction *deleteClip = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), pCore->bin(), SLOT(slotDeleteClip()), QIcon::fromTheme(QStringLiteral("edit-delete"))); deleteClip->setData("delete_clip"); deleteClip->setEnabled(false); QAction *reloadClip = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), pCore->bin(), SLOT(slotReloadClip()), QIcon::fromTheme(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)), QIcon::fromTheme(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()), QIcon::fromTheme(QStringLiteral("edit-file"))); locateClip->setData("locate_clip"); locateClip->setEnabled(false); QAction *duplicateClip = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), pCore->bin(), SLOT(slotDuplicateClip()), QIcon::fromTheme(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); addAction(QStringLiteral("add_project_note"), i18n("Add Project Note"), pCore->projectManager(), SLOT(slotAddProjectNote()), QIcon::fromTheme(QStringLiteral("bookmark"))); QHash actions({{QStringLiteral("locate"), locateClip}, {QStringLiteral("reload"), reloadClip}, {QStringLiteral("duplicate"), duplicateClip}, {QStringLiteral("proxy"), proxyClip}, {QStringLiteral("properties"), clipProperties}, {QStringLiteral("open"), openClip}, {QStringLiteral("delete"), deleteClip}, {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()]; auto allTransitions = TransitionsRepository::get()->getNames(); for (const auto &transition : allTransitions) { auto *transAction = new QAction(transition.first, this); transAction->setData(transition.second); transAction->setIconVisibleInMenu(false); transitionActions->addAction("transition_" + transition.second, transAction); } // monitor actions addAction(QStringLiteral("extract_frame"), i18n("Extract frame..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrame()), QIcon::fromTheme(QStringLiteral("insert-image"))); addAction(QStringLiteral("extract_frame_to_project"), i18n("Extract frame to project..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrameToProject()), QIcon::fromTheme(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::MoviesLocation)); 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() || KdenliveSettings::sdlAudioBackend().isEmpty()) { // First run, check if user is on a KDE Desktop firstRun = true; // this is our first run, show Wizard QPointer w = new Wizard(true, false); 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, config->name().contains(QLatin1String("appimage"))); 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, 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); connect(w, &ProjectSettings::disableProxies, this, &MainWindow::slotDisableProxies); // 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()); bool modified = false; if (m_renderWidget) { m_renderWidget->updateDocumentPath(); } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { slotSwitchAudioThumbs(); } 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->projectItemModel()->clipsCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { pCore->bin()->rebuildProxies(); } } if (project->getDocumentProperty(QStringLiteral("externalproxyparams")) != w->externalProxyParams()) { modified = true; project->setDocumentProperty(QStringLiteral("externalproxyparams"), w->externalProxyParams()); if (pCore->projectItemModel()->clipsCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { 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 (project->getDocumentProperty(QStringLiteral("proxyimagesize")) != QString::number(w->proxyImageSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimagesize"), QString::number(w->proxyImageSize())); } if (QString::number((int)w->useProxy()) != project->getDocumentProperty(QStringLiteral("enableproxy"))) { project->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); modified = true; slotUpdateProxySettings(); } if (QString::number((int)w->useExternalProxy()) != project->getDocumentProperty(QStringLiteral("enableexternalproxy"))) { project->setDocumentProperty(QStringLiteral("enableexternalproxy"), QString::number((int)w->useExternalProxy())); modified = true; } 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) { // Proceed 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 (pCore->getCurrentProfile()->path() != profile || project->profileChanged(profile)) { if (!qFuzzyCompare(pCore->getCurrentProfile()->fps() - ProfileRepository::get()->getProfile(profile)->fps(), 0.)) { // Fps was changed, we save the project to an xml file with updated profile and reload project // Check if blank project if (project->url().fileName().isEmpty() && !project->isModified()) { // Trying to switch project profile from an empty project pCore->setCurrentProfile(profile); pCore->projectManager()->newFile(profile, false); return; } pCore->projectManager()->saveWithUpdatedProfile(profile); } else { pCore->setCurrentProfile(profile); pCore->projectManager()->slotResetProfiles(); slotUpdateDocumentState(true); } } 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(project->useProxy(), this); connect(m_renderWidget, &RenderWidget::shutdown, this, &MainWindow::slotShutdown); connect(m_renderWidget, &RenderWidget::selectedRenderProfile, this, &MainWindow::slotSetDocumentRenderProfile); connect(m_renderWidget, &RenderWidget::abortProcess, this, &MainWindow::abortRenderJob); connect(m_renderWidget, &RenderWidget::openDvdWizard, this, &MainWindow::slotDvdWizard); connect(this, &MainWindow::updateRenderWidgetProfile, m_renderWidget, &RenderWidget::adjustViewToProfile); double projectDuration = GenTime(getMainTimeline()->controller()->duration(), pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(project->getGuideModel()->getAllMarkers(), projectDuration); m_renderWidget->updateDocumentPath(); 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()); } } 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->projectItemModel()->getClipByUrl(QFileInfo(url)); if (!ids.isEmpty()) { // Clip is already in project bin, abort return; } ClipCreator::createClipFromFile(url, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel()); } } void MainWindow::addTimelineClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->projectItemModel()->getClipByUrl(QFileInfo(url)); if (!ids.isEmpty()) { pCore->selectBinClip(ids.constFirst()); slotInsertClipInsert(); } } } 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())); break; default: 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) { Q_UNUSED(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()); m_projectMonitor->slotLoadClipZone(project->zone()); connect(m_projectMonitor, &Monitor::multitrackView, getMainTimeline()->controller(), &TimelineController::slotMultitrackView, Qt::UniqueConnection); connect(getMainTimeline()->controller(), &TimelineController::timelineClipSelected, pCore->library(), &LibraryWidget::enableAddSelection, Qt::UniqueConnection); connect(pCore->library(), &LibraryWidget::saveTimelineSelection, getMainTimeline()->controller(), &TimelineController::saveTimelineSelection, Qt::UniqueConnection); // 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, 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, [this](double changed) { if (changed == 0.0) { slotUpdateProfile(false); } else { slotUpdateProfile(true); } }, Qt::DirectConnection); connect(trackView, &Timeline::zoneMoved, this, &MainWindow::slotZoneMoved); trackView->projectView()->setContextMenu(m_timelineContextMenu, m_timelineClipActions, m_timelineContextTransitionMenu, m_clipTypeGroup, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); */ getMainTimeline()->controller()->clipActions = kdenliveCategoryMap.value(QStringLiteral("timelineselection"))->actions(); 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->updateDocumentPath(); 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); connect(m_effectList2, &EffectListWidget::reloadFavorites, getMainTimeline(), &TimelineWidget::updateEffectFavorites); connect(m_transitionList2, &TransitionListWidget::reloadFavorites, getMainTimeline(), &TimelineWidget::updateTransitionFavorites); // 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) { double projectDuration = GenTime(getMainTimeline()->controller()->duration() - TimelineModel::seekDuration - 2, pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(pCore->currentDoc()->getGuideModel()->getAllMarkers(), projectDuration); } } void MainWindow::slotEditKeys() { KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); // Find the combobox inside KShortcutsDialog for choosing keyboard scheme QComboBox *schemesList = nullptr; foreach (QLabel *label, dialog.findChildren()) { if (label->text() == i18n("Current scheme:")) { schemesList = qobject_cast(label->buddy()); break; } } // If scheme choosing combobox was found, find the "More Actions" button in the same // dialog that provides a dropdown menu with additional actions, and add // "Download New Keyboard Schemes..." button into that menu if (schemesList) { foreach (QPushButton *button, dialog.findChildren()) { if (button->text() == i18n("More Actions")) { QMenu *moreActionsMenu = button->menu(); moreActionsMenu->addAction(i18n("Download New Keyboard Schemes..."), this, [this, schemesList] { slotGetNewKeyboardStuff(schemesList); }); break; } } } else { qWarning() << "Could not get list of schemes. Downloading new schemes is not available."; } 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::doResetConsumer, pCore->projectManager(), &ProjectManager::slotResetConsumers); 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); connect(dialog, &KdenliveSettingsDialog::resetView, this, &MainWindow::resetTimelineTracks); 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()) { 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()); slotSwitchAutomaticTransition(); // Update list of transcoding profiles buildDynamicActions(); loadClipActions(); } void MainWindow::slotSwitchVideoThumbs() { KdenliveSettings::setVideothumbnails(!KdenliveSettings::videothumbnails()); m_timelineTabs->showThumbnailsChanged(); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); } void MainWindow::slotSwitchAudioThumbs() { KdenliveSettings::setAudiothumbnails(!KdenliveSettings::audiothumbnails()); 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) { m_assetPanel->deleteCurrentEffect(); return; } widget = widget->parentWidget(); } // effect stack has no focus getMainTimeline()->controller()->deleteSelectedClips(); } } void MainWindow::slotAddClipMarker() { 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 /* 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 /* 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 occurred while deleting markers"), ErrorMessage); return; } } void MainWindow::slotEditClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { // 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); - getMainTimeline()->controller()->addTrack(-1); + getCurrentTimeline()->controller()->addTrack(-1); } void MainWindow::slotDeleteTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); - getMainTimeline()->controller()->deleteTrack(-1); + getCurrentTimeline()->controller()->deleteTrack(-1); } void MainWindow::slotSelectTrack() { - getMainTimeline()->controller()->selectCurrentTrack(); + getCurrentTimeline()->controller()->selectCurrentTrack(); } void MainWindow::slotSelectAllTracks() { - getMainTimeline()->controller()->selectAll(); + getCurrentTimeline()->controller()->selectAll(); } void MainWindow::slotUnselectAllTracks() { - getMainTimeline()->controller()->clearSelection(); + getCurrentTimeline()->model()->requestClearSelection(); } void MainWindow::slotEditGuide() { - getMainTimeline()->controller()->editGuide(); + getCurrentTimeline()->controller()->editGuide(); } void MainWindow::slotDeleteGuide() { - getMainTimeline()->controller()->switchGuide(-1, true); + getCurrentTimeline()->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; } int pos = getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), true); if (pos > 0) { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } } void MainWindow::slotInsertClipInsert() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } int pos = getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), false); if (pos > 0) { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } } void MainWindow::slotExtractZone() { getMainTimeline()->controller()->extractZone(m_clipMonitor->getZoneInfo()); } void MainWindow::slotLiftZone() { getMainTimeline()->controller()->extractZone(m_clipMonitor->getZoneInfo(), true); } 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() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true); } void MainWindow::slotSelectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true); } void MainWindow::slotDeselectTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, false); } void MainWindow::slotDeselectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, false); } void MainWindow::slotSelectAddTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true, true); } void MainWindow::slotSelectAddTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true, true); } void MainWindow::slotGroupClips() { getCurrentTimeline()->controller()->groupSelection(); } void MainWindow::slotUnGroupClips() { getCurrentTimeline()->controller()->unGroupSelection(); } void MainWindow::slotEditItemDuration() { // 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 /* 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::slotAddEffect(QAction *result) { qDebug() << "// EFFECTS MENU TRIGGERED: " << result->data().toString(); if (!result) { return; } QString effectId = result->data().toString(); addEffect(effectId); } void MainWindow::addEffect(const QString &effectId) { if (m_assetPanel->effectStackOwner().first == ObjectType::TimelineClip) { // Add effect to the current timeline selection QVariantMap effectData; effectData.insert(QStringLiteral("kdenlive/effect"), effectId); pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } else if (m_assetPanel->effectStackOwner().first == ObjectType::TimelineTrack || m_assetPanel->effectStackOwner().first == ObjectType::BinClip) { if (!m_assetPanel->addEffect(effectId)) { pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage); } } else { pCore->displayMessage(i18n("Select an item to add effect"), InformationMessage); } } 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); updateZoomSlider(value); } void MainWindow::updateZoomSlider(int value) { slotUpdateZoomSliderToolTip(value); KdenliveDoc *project = pCore->currentDoc(); if (project) { project->setZoom(value); } m_zoomOut->setEnabled(value < m_zoomSlider->maximum()); m_zoomIn->setEnabled(value > m_zoomSlider->minimum()); QSignalBlocker blocker(m_zoomSlider); m_zoomSlider->setValue(value); } 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) { int max = m_zoomSlider->maximum() + 1; m_zoomSlider->setToolTip(i18n("Zoom Level: %1/%2", max - zoomlevel, max)); } 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); } else { m_clipMonitor->slotStart(); } } void MainWindow::slotClipEnd() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(true); } else { m_clipMonitor->slotEnd(); } } 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) { TimelineMode::EditMode mode = TimelineMode::NormalEdit; if (action == m_overwriteEditTool) { mode = TimelineMode::OverwriteEdit; } else if (action == m_insertEditTool) { mode = TimelineMode::InsertEdit; } getMainTimeline()->controller()->getModel()->setEditMode(mode); } 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() { getMainTimeline()->controller()->pasteEffects(); } void MainWindow::slotClipInTimeline(const QString &clipId, const QList &ids) { Q_UNUSED(clipId) 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->getItemPosition(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(); if (!ids.isEmpty()) { m_projectBinDock->raise(); ObjectId id(ObjectType::TimelineClip, ids.constFirst()); int start = pCore->getItemIn(id); int duration = pCore->getItemDuration(id); QPoint zone(start, start + duration); qDebug() << " - - selecting clip on monitor, zone: " << zone; if (m_projectMonitor->isActive()) { slotSwitchMonitors(); } int pos = m_projectMonitor->position(); int itemPos = pCore->getItemPosition(id); if (pos >= itemPos && pos < itemPos + duration) { pos -= (itemPos - start); } else { pos = -1; } pCore->selectBinClip(getMainTimeline()->controller()->getClipBinId(ids.constFirst()), pos, zone); } } void MainWindow::slotSelectClipInTimeline() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); auto *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::slotGetNewKeyboardStuff(QComboBox *schemesList) { if (getNewStuff(QStringLiteral(":data/kdenlive_keyboardschemes.knsrc")) > 0) { // Refresh keyboard schemes list (schemes list creation code copied from KShortcutSchemesEditor) QStringList schemes; schemes << QStringLiteral("Default"); // List files in the shortcuts subdir, each one is a scheme. See KShortcutSchemesHelper::{shortcutSchemeFileName,exportActionCollection} const QStringList shortcutsDirs = QStandardPaths::locateAll( QStandardPaths::GenericDataLocation, QCoreApplication::applicationName() + QStringLiteral("/shortcuts"), QStandardPaths::LocateDirectory); qCDebug(KDENLIVE_LOG) << "shortcut scheme dirs:" << shortcutsDirs; Q_FOREACH (const QString &dir, shortcutsDirs) { Q_FOREACH (const QString &file, QDir(dir).entryList(QDir::Files | QDir::NoDotAndDotDot)) { qCDebug(KDENLIVE_LOG) << "shortcut scheme file:" << file; schemes << file; } } schemesList->clear(); schemesList->addItems(schemes); } } void MainWindow::slotAutoTransition() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->autoTransition(); } */ } void MainWindow::slotSplitAV() { getMainTimeline()->controller()->splitAV(); } void MainWindow::slotSetAudioAlignReference() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->setAudioAlignReference(); } */ } void MainWindow::slotAlignAudio() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->alignAudio(); } */ } void MainWindow::slotUpdateClipType(QAction *action) { Q_UNUSED(action) // TODO refac /* if (pCore->projectManager()->currentTimeline()) { PlaylistState::ClipState state = (PlaylistState::ClipState)action->data().toInt(); pCore->projectManager()->currentTimeline()->projectView()->setClipType(state); } */ } void MainWindow::slotUpdateTimelineView(QAction *action) { int viewMode = action->data().toInt(); KdenliveSettings::setAudiotracksbelow(viewMode == 1); getMainTimeline()->controller()->getModel()->_resetView(); } 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; std::unique_ptr filter; for (const QString &stab : {QStringLiteral("vidstab"), QStringLiteral("videostab2"), QStringLiteral("videostab")}) { filter = std::make_unique(profile, stab.toUtf8().constData()); if ((filter != nullptr) && filter->is_valid()) { QAction *action = new QAction(i18n("Stabilize (%1)", stab), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [stab]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18np("Stabilize clip", "Stabilize clips", pCore->bin()->selectedClipsIds().size()), stab); }); break; } } filter = std::make_unique(profile, "motion_est"); if (filter) { if (filter->is_valid()) { QAction *action = new QAction(i18n("Automatic scene split"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Scene detection")); }); } } if (true /* TODO: check if timewarp producer is available */) { QAction *action = new QAction(i18n("Duplicate clip with speed change"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Change clip speed")); }); } // 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) { Q_UNUSED(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::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(); QString id; pCore->projectItemModel()->requestAddBinSubClip(id, info.x(), info.y(), QString(), m_clipMonitor->activeClipId()); } 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() { KdenliveDoc *doc = pCore->currentDoc(); QDomDocument xmlDoc = doc->xmlSceneList(m_projectMonitor->sceneList(doc->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile())); QPointer d(new ArchiveWidget(doc->url().fileName(), xmlDoc, getMainTimeline()->controller()->extractCompositionLumas(), 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, &ResourceWidget::addClip, this, &MainWindow::slotAddProjectClip); d->show(); } void MainWindow::slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes) { Q_UNUSED(keyframes) Q_UNUSED(tag) 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, [this](Qt::DockWidgetArea dockLocationArea) { if (dockLocationArea == Qt::NoDockWidgetArea) { updateDockTitleBars(false); } else { updateDockTitleBars(true); } }); 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)); } 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; } 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); } } } void MainWindow::slotToggleAutoPreview(bool enable) { KdenliveSettings::setAutopreview(enable); if (enable && getMainTimeline()) { getMainTimeline()->controller()->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 auto *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); auto *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 (int i : progression) { Q_FOREACH (int it, avSizes) { if (it >= 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 the 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){ Q_UNUSED(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::resetTimelineTracks() { TimelineWidget *current = getCurrentTimeline(); if (current) { current->controller()->resetTrackHeight(); } } void MainWindow::slotChangeSpeed(int speed) { ObjectId owner = m_assetPanel->effectStackOwner(); // TODO: manage bin clips / tracks if (owner.first == ObjectType::TimelineClip) { getCurrentTimeline()->controller()->changeItemSpeed(owner.second, speed); } } void MainWindow::slotSwitchTimelineZone(bool active) { pCore->currentDoc()->setDocumentProperty(QStringLiteral("enableTimelineZone"), active ? QStringLiteral("1") : QStringLiteral("0")); getCurrentTimeline()->controller()->useRulerChanged(); QSignalBlocker blocker(m_useTimelineZone); m_useTimelineZone->setActive(active); } void MainWindow::slotGrabItem() { getCurrentTimeline()->controller()->grabCurrent(); } // static void MainWindow::refreshLumas() { // Check for Kdenlive installed luma files, add empty string at start for no luma QStringList imagefiles; QStringList fileFilters; MainWindow::m_lumaFiles.clear(); fileFilters << QStringLiteral("*.png") << QStringLiteral("*.pgm"); QStringList customLumas = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("lumas"), QStandardPaths::LocateDirectory); customLumas.append(QString(mlt_environment("MLT_DATA")) + QStringLiteral("/lumas")); for (const QString &folder : customLumas) { QDir topDir(folder); QStringList folders = topDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); for (const QString &f : folders) { QDir dir(topDir.absoluteFilePath(f)); QStringList filesnames = dir.entryList(fileFilters, QDir::Files); if (MainWindow::m_lumaFiles.contains(f)) { imagefiles = MainWindow::m_lumaFiles.value(f); } for (const QString &fname : filesnames) { imagefiles.append(dir.absoluteFilePath(fname)); } MainWindow::m_lumaFiles.insert(f, imagefiles); } } } #ifdef DEBUG_MAINW #undef DEBUG_MAINW #endif diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index c0a54486f..f3fc707a0 100644 --- a/src/timeline2/model/clipmodel.cpp +++ b/src/timeline2/model/clipmodel.cpp @@ -1,688 +1,687 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "clipmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "macros.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include #include #include #include ClipModel::ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, double speed) : MoveableItem(parent, id) , m_producer(std::move(prod)) , m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack)) , m_binClipId(binClipId) , forceThumbReload(false) , m_currentState(state) , m_speed(speed) , m_fakeTrack(-1) { m_producer->set("kdenlive:id", binClipId.toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); m_canBeVideo = binClip->hasVideo(); m_canBeAudio = binClip->hasAudio(); m_clipType = binClip->clipType(); if (binClip) { m_endlessResize = !binClip->hasLimitedDuration(); } else { m_endlessResize = false; } QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) { qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles; if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles; ptr->dataChanged(ix, ix, roles); } } }); } int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed) { id = (id == -1 ? TimelineModel::getNextId() : id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); std::shared_ptr cutProducer = binClip->getTimelineProducer(-1, id, state, speed); std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed)); clip->setClipState_lambda(state)(); parent->registerClip(clip); return id; } int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer, PlaylistState::ClipState state) { // we hand the producer to the bin clip, and in return we get a cut to a good master producer // We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other // clipModel (due to a mlt limitation, see ProjectClip doc) int id = TimelineModel::getNextId(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); double speed = 1.0; if (QString::fromUtf8(producer->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = producer->parent().get_double("warp_speed"); } auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state); std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id, state, speed)); clip->setClipState_lambda(state)(); clip->m_effectStack->importEffects(producer, state, result.second); parent->registerClip(clip); return id; } void ClipModel::registerClipToBin(std::shared_ptr service, bool registerProducer) { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (!binClip) { qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!"; } qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count(); binClip->registerService(m_parent, m_id, std::move(service), registerProducer); } void ClipModel::deregisterClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); binClip->deregisterTimelineClip(m_id); - pCore->removeFromSelection(m_id); } ClipModel::~ClipModel() = default; bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" << // m_producer->get_length(); if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) { return false; } int delta = getPlaytime() - size; if (delta == 0) { return true; } int in = m_producer->get_in(); int out = m_producer->get_out(); int old_in = in, old_out = out; // check if there is enough space on the chosen side if (!right && in + delta < 0 && !m_endlessResize) { return false; } if (!m_endlessResize && right && (out - delta >= m_producer->get_length())) { return false; } if (right) { out -= delta; } else { in += delta; } // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out; std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; int outPoint = out; int inPoint = in; int offset = 0; if (m_endlessResize) { offset = inPoint; outPoint = out - in; inPoint = 0; } if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right); } else { qDebug() << "Error : Moving clip failed because parent timeline is not available anymore"; Q_ASSERT(false); } } else { // Ensure producer is long enough if (m_endlessResize && outPoint > m_producer->parent().get_length()) { m_producer->set("length", outPoint + 1); } } Fun operation = [this, inPoint, outPoint, track_operation]() { if (track_operation()) { m_producer->set_in_and_out(inPoint, outPoint); return true; } return false; }; if (operation()) { // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here if (m_currentTrackId != -1) { QVector roles{TimelineModel::DurationRole}; if (!right) { roles.push_back(TimelineModel::StartRole); roles.push_back(TimelineModel::InPointRole); } else { roles.push_back(TimelineModel::OutPointRole); } if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); // TODO: integrate in undo ptr->dataChanged(ix, ix, roles); track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right); } } Fun reverse = [this, old_in, old_out, track_reverse]() { if (track_reverse()) { m_producer->set_in_and_out(old_in, old_out); return true; } return false; }; qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", " << m_producer->get_playtime(); if (logUndo) { adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString ClipModel::getProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return QString::fromUtf8(service()->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(service()->get(name.toUtf8().constData())); } int ClipModel::getIntProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_int(name.toUtf8().constData()); } return service()->get_int(name.toUtf8().constData()); } QSize ClipModel::getFrameSize() const { READ_LOCK(); if (service()->parent().is_valid()) { return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height")); } return {service()->get_int("meta.media.width"), service()->get_int("meta.media.height")}; } double ClipModel::getDoubleProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_double(name.toUtf8().constData()); } return service()->get_double(name.toUtf8().constData()); } Mlt::Producer *ClipModel::service() const { READ_LOCK(); return m_producer.get(); } std::shared_ptr ClipModel::getProducer() { READ_LOCK(); return m_producer; } int ClipModel::getPlaytime() const { READ_LOCK(); return m_producer->get_playtime(); } void ClipModel::setTimelineEffectsEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStack->setEffectStackEnabled(enabled); } bool ClipModel::addEffect(const QString &effectId) { QWriteLocker locker(&m_lock); if (EffectsRepository::get()->getType(effectId) == EffectType::Audio) { if (m_currentState == PlaylistState::VideoOnly) { return false; } } else if (m_currentState == PlaylistState::AudioOnly) { return false; } m_effectStack->appendEffect(effectId); return true; } bool ClipModel::copyEffect(const std::shared_ptr &stackModel, int rowId) { QWriteLocker locker(&m_lock); m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), m_currentState); return true; } bool ClipModel::importEffects(std::shared_ptr stackModel) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(std::move(stackModel), m_currentState); return true; } bool ClipModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(std::move(service), m_currentState); return true; } bool ClipModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); m_effectStack->removeFade(fromStart); return true; } bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo); } bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName; Fun operation = [this, duration, effectName, originalDuration]() { return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly(), originalDuration > 0); }; if (operation() && originalDuration > 0) { Fun reverse = [this, originalDuration, effectName]() { return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly(), true); }; UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return true; } bool ClipModel::audioEnabled() const { READ_LOCK(); return stateToBool(m_currentState).second; } bool ClipModel::isAudioOnly() const { READ_LOCK(); return m_currentState == PlaylistState::AudioOnly; } void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state, double speed) { // We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip // first, refresh, and then replant. QWriteLocker locker(&m_lock); int in = getIn(); int out = getOut(); qDebug() << "refresh " << speed << m_speed << in << out; if (!qFuzzyCompare(speed, m_speed) && !qFuzzyCompare(speed, 0.)) { in = in * std::abs(m_speed / speed); out = in + getPlaytime() - 1; // prevent going out of the clip's range out = std::min(out, int(double(m_producer->get_length()) * std::abs(m_speed / speed)) - 1); m_speed = speed; qDebug() << "changing speed" << in << out << m_speed; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); std::shared_ptr binProducer = binClip->getTimelineProducer(m_currentTrackId, m_id, state, m_speed); m_producer = std::move(binProducer); m_producer->set_in_and_out(in, out); // replant effect stack in updated service m_effectStack->resetService(m_producer); m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); m_endlessResize = !binClip->hasLimitedDuration(); } void ClipModel::refreshProducerFromBin() { refreshProducerFromBin(m_currentState); } bool ClipModel::useTimewarpProducer(double speed, Fun &undo, Fun &redo) { if (m_endlessResize) { // no timewarp for endless producers return false; } if (qFuzzyCompare(speed, m_speed)) { // nothing to do return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; double previousSpeed = getSpeed(); int oldDuration = getPlaytime(); int newDuration = int(double(oldDuration) * std::abs(previousSpeed / speed)); int oldOut = getOut(); int oldIn = getIn(); auto operation = useTimewarpProducer_lambda(speed); auto reverse = useTimewarpProducer_lambda(previousSpeed); if (oldOut >= newDuration) { // in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer reverse = [reverse, oldIn, oldOut, this]() { bool res = reverse(); if (res) { setInOut(oldIn, oldOut); } return res; }; } if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); bool res = requestResize(newDuration, true, local_undo, local_redo, true); if (!res) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } qDebug() << "tw: operation fail"; return false; } Fun ClipModel::useTimewarpProducer_lambda(double speed) { QWriteLocker locker(&m_lock); return [speed, this]() { qDebug() << "timeWarp producer" << speed; refreshProducerFromBin(m_currentState, speed); if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); ptr->notifyChange(ix, ix, TimelineModel::SpeedRole); } return true; }; } QVariant ClipModel::getAudioWaveform() { READ_LOCK(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (binClip) { return QVariant::fromValue(binClip->audioFrameCache); } return QVariant(); } const QString &ClipModel::binId() const { return m_binClipId; } std::shared_ptr ClipModel::getMarkerModel() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel(); } int ClipModel::audioChannels() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels(); } int ClipModel::fadeIn() const { return m_effectStack->getFadePosition(true); } int ClipModel::fadeOut() const { return m_effectStack->getFadePosition(false); } double ClipModel::getSpeed() const { return m_speed; } KeyframeModel *ClipModel::getKeyframeModel() { return m_effectStack->getEffectKeyframeModel(); } bool ClipModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void ClipModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } void ClipModel::setCurrentTrackId(int tid, bool finalMove) { if (tid == m_currentTrackId) { return; } MoveableItem::setCurrentTrackId(tid, finalMove); if (finalMove && tid != -1) { refreshProducerFromBin(m_currentState); } } Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); return [this, state]() { if (auto ptr = m_parent.lock()) { switch (state) { case PlaylistState::Disabled: m_producer->set("set.test_audio", 1); m_producer->set("set.test_image", 1); break; case PlaylistState::VideoOnly: m_producer->set("set.test_image", 0); break; case PlaylistState::AudioOnly: m_producer->set("set.test_audio", 0); break; default: // error break; } m_currentState = state; if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case QModelIndex ix = ptr->makeClipIndexFromID(m_id); ptr->dataChanged(ix, ix, {TimelineModel::StatusRole}); } return true; } return false; }; } bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo) { if (state == PlaylistState::VideoOnly && !canBeVideo()) { return false; } if (state == PlaylistState::AudioOnly && !canBeAudio()) { return false; } if (state == m_currentState) { return true; } auto old_state = m_currentState; auto operation = setClipState_lambda(state); if (operation()) { auto reverse = setClipState_lambda(old_state); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } PlaylistState::ClipState ClipModel::clipState() const { READ_LOCK(); return m_currentState; } ClipType::ProducerType ClipModel::clipType() const { READ_LOCK(); return m_clipType; } void ClipModel::passTimelineProperties(const std::shared_ptr &other) { READ_LOCK(); Mlt::Properties source(m_producer->get_properties()); Mlt::Properties dest(other->service()->get_properties()); dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect"); } bool ClipModel::canBeVideo() const { return m_canBeVideo; } bool ClipModel::canBeAudio() const { return m_canBeAudio; } const QString ClipModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } int ClipModel::getFakeTrackId() const { return m_fakeTrack; } void ClipModel::setFakeTrackId(int fid) { m_fakeTrack = fid; } int ClipModel::getFakePosition() const { return m_fakePosition; } void ClipModel::setFakePosition(int fid) { m_fakePosition = fid; } QDomElement ClipModel::toXml(QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("clip")); container.setAttribute(QStringLiteral("binid"), m_binClipId); container.setAttribute(QStringLiteral("id"), m_id); container.setAttribute(QStringLiteral("in"), getIn()); container.setAttribute(QStringLiteral("out"), getOut()); container.setAttribute(QStringLiteral("position"), getPosition()); container.setAttribute(QStringLiteral("state"), (int)m_currentState); if (auto ptr = m_parent.lock()) { int trackId = ptr->getTrackPosition(m_currentTrackId); container.setAttribute(QStringLiteral("track"), trackId); if (ptr->isAudioTrack(getCurrentTrackId())) { container.setAttribute(QStringLiteral("audioTrack"), 1); int mirrorId = ptr->getMirrorVideoTrackId(m_currentTrackId); if (mirrorId > -1) { mirrorId = ptr->getTrackPosition(mirrorId); } container.setAttribute(QStringLiteral("mirrorTrack"), mirrorId); } } container.setAttribute(QStringLiteral("speed"), m_speed); container.appendChild(m_effectStack->toXml(document)); return container; } bool ClipModel::checkConsistency() { if (!m_effectStack->checkConsistency()) { qDebug() << "Consistency check failed for effecstack"; return false; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); auto instances = binClip->timelineInstances(); bool found = false; for (const auto &i : instances) { if (i == m_id) { found = true; break; } } if (!found) { qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence"; return false; } if (m_currentState == PlaylistState::VideoOnly && !m_canBeVideo) { qDebug() << "ERROR: clip is in video state but doesn't have video"; return false; } if (m_currentState == PlaylistState::AudioOnly && !m_canBeAudio) { qDebug() << "ERROR: clip is in video state but doesn't have video"; return false; } // TODO: check speed return true; } diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index 4e0092efb..378baa403 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,1002 +1,1284 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "timelinefunctions.hpp" +#include "bin/bin.h" +#include "bin/projectclip.h" +#include "bin/projectfolder.h" +#include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" +#include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include +#include -bool TimelineFunctions::copyClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, - Fun &redo) +bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, + Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack, state); return res; } bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (const QString &binId : binIds) { int clipId; if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, true, undo, redo)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); - bool res = copyClip(timeline, clipId, newId, state, undo, redo); + bool res = cloneClip(timeline, clipId, newId, state, undo, redo); timeline->m_blockRefresh = true; res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } timeline->m_blockRefresh = false; return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = TimelineFunctions::requestClipCut(std::move(timeline), clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clips = timeline->getGroupElements(clipId); - int root = timeline->m_groups->getRootId(clipId); + timeline->requestClearSelection(); std::unordered_set topElements; - if (timeline->m_temporarySelectionGroup == root) { - topElements = timeline->m_groups->getDirectChildren(root); - } else { - topElements.insert(root); - } + std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); + // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) - bool processClearSelection = false; + timeline->requestClearSelection(); int count = 0; QList newIds; int mainId = -1; QList clipsToCut; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { clipsToCut << cid; - if (!processClearSelection && pCore->isSelected(cid)) { - processClearSelection = true; - } } } - if (processClearSelection) { - pCore->clearSelection(); - } for (int cid : clipsToCut) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } else if (cid == clipId) { mainId = newId; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); newIds << newId; } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hierarchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; bool res = true; for (const int topId : topElements) { res = res & timeline->m_groups->split(topId, criterion, undo, redo); } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } - if (processClearSelection) { - if (mainId >= 0) { - pCore->selectItem(mainId); - } else if (!newIds.isEmpty()) { - pCore->selectItem(newIds.first()); - } - } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1); if (clips.size() > 0) { timeline->requestClipsGroup(clips, false, GroupType::Selection); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(itemId); bool isClip = timeline->isClip(itemId); if (isClip) { timeline->requestClipMove(itemId, track, startPosition, false, false); } else { timeline->requestCompositionMove(itemId, track, startPosition, false, false); } std::unordered_set clips = timeline->getGroupElements(itemId); - // break group - pCore->clearSelection(); + // clear selection + timeline->requestClearSelection(); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; int res = timeline->requestClipsGroup(clips, undo, redo); bool final = false; if (res > -1) { if (clips.size() > 1) { final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved if (isClip) { final = timeline->requestClipMove(itemId, track, endPosition, true, true, undo, redo); } else { final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo); } } } if (final && clips.size() > 1) { final = timeline->requestClipUngroup(itemId, undo, redo); } if (final) { pCore->pushUndo(undo, redo, i18n("Insert space")); return true; } return false; } bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = true; for (int trackId : tracks) { result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); } if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo); } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; int trackId = trackIds.takeFirst(); if (overwrite) { result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (!trackIds.isEmpty()) { result = result && TimelineFunctions::liftZone(timeline, trackIds.takeFirst(), QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } } else { // Cut all tracks auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->isLocked()) { ++it; continue; } int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } ++it; } result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } if (result) { int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo); if (result) { pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } } if (!result) { undo(); } return result; } bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId; TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); qDebug() << "/// CUTTING AT START DONE"; } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId; TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); qDebug() << "/// CUTTING AT END DONE"; } } std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } if (!result) { undo(); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::insertSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.x(), -1, true); bool result = true; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } else { pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); - res = copyClip(timeline, id, newId, state, undo, redo); + res = cloneClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); std::unique_ptr transProps(timeline->m_allCompositions[id]->properties()); res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Successful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId) { PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); PlaylistState::ClipState state = PlaylistState::Disabled; bool disable = true; if (oldState == PlaylistState::Disabled) { state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType(); disable = false; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = changeClipState(timeline, clipId, state, undo, redo); if (result) { pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); } return result; } bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) { int track = timeline->getClipTrackId(clipId); int start = -1; int end = -1; if (track > -1) { if (!timeline->getTrackById_const(track)->isAudioTrack()) { start = timeline->getItemPosition(clipId); end = start + timeline->getItemPlaytime(clipId); } } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); Fun local_update = [start, end, timeline]() { if (start > -1) { timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return true; }; if (start > -1) { local_update(); PUSH_LAMBDA(local_update, local_redo); PUSH_LAMBDA(local_update, local_undo); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups - pCore->clearSelection(); + timeline->requestClearSelection(false, undo, redo); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage); return false; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; - bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); + bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { + timeline->requestSetSelection(clips, undo, redo); pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups - pCore->clearSelection(); + timeline->requestClearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); QList possibleTracks = QList() << videoTarget; if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage); return false; } int newId; - bool res = copyClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); + bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Video")); } return done; } void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = compo->getForcedTrack() == -1; bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable) { QList videoTracks; for (const auto &track : timeline->m_iteratorTable) { if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) { continue; } videoTracks << track.first; } if (videoTracks.size() < 2) { pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage); } // First, dis/enable track compositing QScopedPointer service(timeline->m_tractor->field()); Mlt::Field *field = timeline->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); int added = t.get_int("internal_added"); if (added == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions t.set("disable", enable ? "1" : nullptr); } else if (!enable && added == 200) { field->disconnect_service(t); } } service.reset(service->producer()); } if (enable) { for (int i = 0; i < videoTracks.size(); ++i) { Mlt::Transition transition(*timeline->m_tractor->profile(), "composite"); transition.set("mlt_service", "composite"); transition.set("a_track", 0); transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i))); transition.set("distort", 0); transition.set("aligned", 0); // 200 is an arbitrary number so we can easily remove these transition later transition.set("internal_added", 200); QString geometry; switch (i) { case 0: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("0 0 50% 100%"); break; case 3: geometry = QStringLiteral("0 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 0 33% 50%"); break; default: geometry = QStringLiteral("0 0 33% 33%"); break; } break; case 1: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("50% 0 50% 100%"); break; case 3: geometry = QStringLiteral("33% 0 33% 100%"); break; case 4: geometry = QStringLiteral("50% 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("33% 0 33% 50%"); break; default: geometry = QStringLiteral("33% 0 33% 33%"); break; } break; case 2: switch (videoTracks.size()) { case 3: geometry = QStringLiteral("66% 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("66% 0 33% 50%"); break; default: geometry = QStringLiteral("66% 0 33% 33%"); break; } break; case 3: switch (videoTracks.size()) { case 4: geometry = QStringLiteral("50% 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 50% 33% 50%"); break; default: geometry = QStringLiteral("0 33% 33% 33%"); break; } break; case 4: switch (videoTracks.size()) { case 5: case 6: geometry = QStringLiteral("33% 50% 33% 50%"); break; default: geometry = QStringLiteral("33% 33% 33% 33%"); break; } break; case 5: switch (videoTracks.size()) { case 6: geometry = QStringLiteral("66% 50% 33% 50%"); break; default: geometry = QStringLiteral("66% 33% 33% 33%"); break; } break; case 6: geometry = QStringLiteral("0 66% 33% 33%"); break; case 7: geometry = QStringLiteral("33% 66% 33% 33%"); break; default: geometry = QStringLiteral("66% 66% 33% 33%"); break; } // Add transition to track: transition.set("geometry", geometry.toUtf8().constData()); transition.set("always_active", 1); field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i))); } } field->unlock(); timeline->requestMonitorRefresh(); } -void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, QList selection, const QDir &targetDir) +void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection, + const QDir &targetDir) { bool ok; QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal, QString(), &ok); if (name.isEmpty() || !ok) { return; } if (targetDir.exists(name + QStringLiteral(".mlt"))) { // TODO: warn and ask for overwrite / rename } int offset = -1; int lowerAudioTrack = -1; int lowerVideoTrack = -1; QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt")); // Build a copy of selected tracks. QMap sourceTracks; for (int i : selection) { int sourceTrack = timeline->getItemTrackId(i); int clipPos = timeline->getItemPosition(i); if (offset < 0 || clipPos < offset) { offset = clipPos; } int trackPos = timeline->getTrackMltIndex(sourceTrack); if (!sourceTracks.contains(trackPos)) { sourceTracks.insert(trackPos, sourceTrack); } } // Build target timeline Mlt::Tractor newTractor(*timeline->m_tractor->profile()); QScopedPointer field(newTractor.field()); int ix = 0; QString composite = TransitionsRepository::get()->getCompositingTransition(); QMapIterator i(sourceTracks); QList compositions; while (i.hasNext()) { i.next(); QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile())); newTractor.set_track(*newTrackPlaylist, ix); // QScopedPointer trackProducer(newTractor.track(ix)); int trackId = i.value(); sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if (isAudio) { newTrackPlaylist->set("hide", 1); if (lowerAudioTrack < 0) { lowerAudioTrack = ix; } } else { newTrackPlaylist->set("hide", 2); if (lowerVideoTrack < 0) { lowerVideoTrack = ix; } } for (int itemId : selection) { if (timeline->getItemTrackId(itemId) == trackId) { // Copy clip on the destination track if (timeline->isClip(itemId)) { int clip_position = timeline->m_allClips[itemId]->getPosition(); auto clip_loc = track->getClipIndexAt(clip_position); int target_clip = clip_loc.second; QSharedPointer clip = track->getClipProducer(target_clip); newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1); } else if (timeline->isComposition(itemId)) { // Composition auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get()); QString id(t->get("kdenlive_id")); QString internal(t->get("internal_added")); if (internal.isEmpty()) { compositions << t; if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service"); t->set("kdenlive_id", t->get("mlt_service")); } } } } } ix++; } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) { Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service")); Mlt::Properties sourceProps(t->get_properties()); newComposition.inherit(sourceProps); QString id(t->get("kdenlive_id")); int in = qMax(0, t->get_in() - offset); int out = t->get_out() - offset; newComposition.set_in_and_out(in, out); int a_track = sourceTracks.value(t->get_a_track()); int b_track = sourceTracks.value(t->get_b_track()); field->plant_transition(newComposition, a_track, b_track); } } } // Track compositing i.toFront(); ix = 0; while (i.hasNext()) { i.next(); int trackId = i.value(); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) { // add track compositing / mix Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData()); if (isAudio) { t.set("sum", 1); } t.set("always_active", 1); t.set("internal_added", 237); field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix); } ix++; } Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.connect(newTractor); xmlConsumer.run(); } int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack) { qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack; int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); int destTrackMltIndex = timeline->getTrackMltIndex(destTrack); int offset = 0; qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex; if (masterTrackMltIndex == destTrackMltIndex) { return offset; } int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1; bool isAudio = timeline->isAudioTrack(startTrack); int track = masterTrackMltIndex; while (track != destTrackMltIndex) { track += step; qDebug() << "+ + +TESTING TRACK: " << track; int trackId = timeline->getTrackIndexFromPosition(track - 1); if (isAudio == timeline->isAudioTrack(trackId)) { offset += step; } } return offset; } int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset) { int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); bool isAudio = timeline->isAudioTrack(startTrack); if (isAudio != audioOffset) { offset = -offset; } qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset; while (offset != 0) { masterTrackMltIndex += offset > 0 ? 1 : -1; qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex; if (masterTrackMltIndex < 0) { masterTrackMltIndex = 0; break; } else if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) { masterTrackMltIndex = (int)timeline->m_allTracks.size(); break; } int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); if (timeline->isAudioTrack(trackId) == isAudio) { offset += offset > 0 ? -1 : 1; } } return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); } QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline) { QList audioTracks; QList videoTracks; for (const auto &track : timeline->m_allTracks) { if (track->isAudioTrack()) { audioTracks << track->getId(); } else { videoTracks << track->getId(); } } return {audioTracks, videoTracks}; } + +QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set itemIds) +{ + int clipId = *(itemIds.begin()); + timeline->requestClearSelection(); + // TODO better guess for master track + int masterTid = timeline->getItemTrackId(clipId); + bool audioCopy = timeline->isAudioTrack(masterTid); + int masterTrack = timeline->getTrackPosition(masterTid); + std::unordered_set groupRoots; + std::transform(itemIds.begin(), itemIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); + + qDebug() << "==============\n GROUP ROOTS: "; + for (int gp : groupRoots) { + qDebug() << "GROUP: " << gp; + } + qDebug() << "\n======="; + QDomDocument copiedItems; + int offset = -1; + QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); + copiedItems.appendChild(container); + QStringList binIds; + for (int id : itemIds) { + if (offset == -1 || timeline->getItemPosition(id) < offset) { + offset = timeline->getItemPosition(id); + } + if (timeline->isClip(id)) { + container.appendChild(timeline->m_allClips[id]->toXml(copiedItems)); + const QString bid = timeline->m_allClips[id]->binId(); + if (!binIds.contains(bid)) { + binIds << bid; + } + } else if (timeline->isComposition(id)) { + container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems)); + } else { + Q_ASSERT(false); + } + } + QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); + container.appendChild(container2); + for (const QString &id : binIds) { + std::shared_ptr clip = pCore->bin()->getBinClip(id); + QDomDocument tmp; + container2.appendChild(clip->toXml(tmp)); + } + container.setAttribute(QStringLiteral("offset"), offset); + if (audioCopy) { + int masterMirror = timeline->getMirrorVideoTrackId(masterTid); + if (masterMirror == -1) { + QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); + if (!projectTracks.second.isEmpty()) { + masterTrack = timeline->getTrackPosition(projectTracks.second.first()); + } + } else { + masterTrack = timeline->getTrackPosition(masterMirror); + } + } + /* masterTrack contains the reference track over which we want to paste. + this is a video track, unless audioCopy is defined */ + container.setAttribute(QStringLiteral("masterTrack"), masterTrack); + container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); + QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); + container.appendChild(grp); + grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots))); + // TODO: groups + qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; + return copiedItems.toString(); +} + +bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position) +{ + QDomDocument copiedItems; + copiedItems.setContent(pasteString); + if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { + qDebug() << " / / READING CLIPS FROM CLIPBOARD"; + } else { + return false; + } + std::function undo = []() { return true; }; + std::function redo = []() { return true; }; + const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); + QMap mappedIds; + // Check available tracks + QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); + int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt(); + QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); + QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); + // find paste tracks + // List of all source audio tracks + QList audioTracks; + // List of all source video tracks + QList videoTracks; + // List of all audio tracks with their corresponding video mirror + QMap audioMirrors; + // List of all source audio tracks that don't have video mirror + QList singleAudioTracks; + for (int i = 0; i < clips.count(); i++) { + QDomElement prod = clips.at(i).toElement(); + int trackPos = prod.attribute(QStringLiteral("track")).toInt(); + bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack")); + if (audioTrack) { + if (!audioTracks.contains(trackPos)) { + audioTracks << trackPos; + } + int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt(); + if (videoMirror == -1) { + if (singleAudioTracks.contains(trackPos)) { + continue; + } + singleAudioTracks << trackPos; + continue; + } + audioMirrors.insert(trackPos, videoMirror); + if (videoTracks.contains(videoMirror)) { + continue; + } + videoTracks << videoMirror; + } else { + if (videoTracks.contains(trackPos)) { + continue; + } + videoTracks << trackPos; + } + } + for (int i = 0; i < compositions.count(); i++) { + QDomElement prod = compositions.at(i).toElement(); + int trackPos = prod.attribute(QStringLiteral("track")).toInt(); + if (!videoTracks.contains(trackPos)) { + videoTracks << trackPos; + } + int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt(); + if (atrackPos == 0 || videoTracks.contains(atrackPos)) { + continue; + } + videoTracks << atrackPos; + } + // Now we have a list of all source tracks, check that we have enough target tracks + qSort(videoTracks); + qSort(audioTracks); + qSort(singleAudioTracks); + int requestedVideoTracks = videoTracks.isEmpty() ? 0 : videoTracks.last() - videoTracks.first() + 1; + int requestedAudioTracks = audioTracks.isEmpty() ? 0 : audioTracks.last() - audioTracks.first() + 1; + if (requestedVideoTracks > projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) { + pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); + return false; + } + + // Check we have enough tracks above/below + if (requestedVideoTracks > 0) { + qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks; + int tracksBelow = masterSourceTrack - videoTracks.first(); + int tracksAbove = videoTracks.last() - masterSourceTrack; + qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove; + qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId; + qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId); + if (projectTracks.second.indexOf(trackId) < tracksBelow) { + qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; + // not enough tracks below, try to paste on upper track + trackId = projectTracks.second.at(tracksBelow); + } else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) { + // not enough tracks above, try to paste on lower track + qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove); + trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1); + } + } + QMap tracksMap; + int masterIx = projectTracks.second.indexOf(trackId); + qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId; + qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack; + qDebug() << "/// MASTER PASTE: " << masterIx; + for (int tk : videoTracks) { + tracksMap.insert(tk, projectTracks.second.at(masterIx + tk - masterSourceTrack)); + qDebug() << "// TK MAP: " << tk << " => " << projectTracks.second.at(masterIx + tk - masterSourceTrack); + } + QMapIterator it(audioMirrors); + while (it.hasNext()) { + it.next(); + int videoIx = tracksMap.value(it.value()); + // qDebug()<<"// TK AUDIO MAP: "< "<getMirrorAudioTrackId(videoIx); + tracksMap.insert(it.key(), timeline->getMirrorAudioTrackId(videoIx)); + } + for (int i = 0; i < singleAudioTracks.size(); i++) { + tracksMap.insert(singleAudioTracks.at(i), projectTracks.first.at(i)); + } + qDebug() << "++++++++++++++++++++++++++\n\n\n// AUDIO MIRRORS: " << audioMirrors << ", RESULT: " << tracksMap; + if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { + // paste from another document, import bin clips + QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); + if (folderId.isEmpty()) { + // Folder doe not exist + const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); + folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); + pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); + } + QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); + for (int i = 0; i < binClips.count(); ++i) { + QDomElement currentProd = binClips.item(i).toElement(); + QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); + if (!pCore->projectItemModel()->isIdFree(clipId)) { + QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); + Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); + mappedIds.insert(clipId, updatedId); + clipId = updatedId; + } + pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo); + } + } + + int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); + + bool res = true; + QLocale locale; + std::unordered_map correspondingIds; + QList waitingIds; + for (int i = 0; i < clips.count(); i++) { + waitingIds << i; + } + for (int i = 0; res && !waitingIds.isEmpty();) { + if (i >= waitingIds.size()) { + i = 0; + } + QDomElement prod = clips.at(waitingIds.at(i)).toElement(); + QString originalId = prod.attribute(QStringLiteral("binid")); + if (mappedIds.contains(originalId)) { + // Map id + originalId = mappedIds.value(originalId); + } + int in = prod.attribute(QStringLiteral("in")).toInt(); + int out = prod.attribute(QStringLiteral("out")).toInt(); + int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); + int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; + double speed = locale.toDouble(prod.attribute(QStringLiteral("speed"))); + int newId; + bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, undo, redo); + if (created) { + // Master producer is ready + // ids.removeAll(originalId); + waitingIds.removeAt(i); + } else { + i++; + qApp->processEvents(); + continue; + } + if (timeline->m_allClips[newId]->m_endlessResize) { + out = out - in; + in = 0; + timeline->m_allClips[newId]->m_producer->set("length", out + 1); + } + timeline->m_allClips[newId]->setInOut(in, out); + int targetId = prod.attribute(QStringLiteral("id")).toInt(); + correspondingIds[targetId] = newId; + res = res & timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo); + // paste effects + if (res) { + std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); + destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo); + } + } + + // Compositions + for (int i = 0; res && i < compositions.count(); i++) { + QDomElement prod = compositions.at(i).toElement(); + QString originalId = prod.attribute(QStringLiteral("composition")); + int in = prod.attribute(QStringLiteral("in")).toInt(); + int out = prod.attribute(QStringLiteral("out")).toInt(); + int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); + int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt(); + if (aTrackId > 0) { + aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId)); + } + int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; + int newId; + auto transProps = std::make_unique(); + QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); + for (int j = 0; j < props.count(); j++) { + transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), + props.at(j).toElement().text().toUtf8().constData()); + } + res = timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in, std::move(transProps), newId, undo, redo); + } + if (!res) { + undo(); + return false; + } + const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); + // Rebuild groups + timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo); + pCore->pushUndo(undo, redo, i18n("Paste clips")); + return true; +} diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp index 0aa9be2d1..feefe327b 100644 --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -1,106 +1,111 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMELINEFUNCTIONS_H #define TIMELINEFUNCTIONS_H #include "definitions.h" #include "undohelper.hpp" #include #include #include /** * @namespace TimelineFunction * @brief This namespace contains a list of static methods for advanced timeline editing features * based on timelinemodel methods */ class TimelineItemModel; struct TimelineFunctions { /* @brief Cuts a clip at given position If the clip is part of the group, all clips of the groups are cut at the same position. The group structure is then preserved for clips on both sides Returns true on success @param timeline : ptr to the timeline model @param clipId: Id of the clip to split @param position: position (in frames from the beginning of the timeline) where to cut */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position); /* This is the same function, except that it accumulates undo/redo */ static bool requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo); /* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */ static bool processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); - /* @brief Makes a perfect copy of a given clip, but do not insert it */ - static bool copyClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + /* @brief Makes a perfect clone of a given clip, but do not insert it */ + static bool cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + + /* @brief Creates a string representation of the given clips, that can then be pasted using pasteClips(). Return an empty string on failure */ + static QString copyClips(const std::shared_ptr &timeline, const std::unordered_set itemIds); + /* @brief Paste the clips as described by the string. Returns true on success*/ + static bool pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position); /* @brief Request the addition of multiple clips to the timeline * If the addition of any of the clips fails, the entire operation is undone. * @returns true on success, false otherwise. * @param binIds the list of bin ids to be inserted * @param trackId the track where the insertion should happen * @param position the position at which the clips should be inserted * @param clipIds a return parameter with the ids assigned to the clips if success, empty otherwise */ static bool requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView); static int requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position); static bool requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition); static bool extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly); static bool liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite); static bool requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position); static void showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value); static void showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value); /* @brief If the clip is activated, disable, otherwise enable * @param timeline: pointer to the timeline that we modify * @param clipId: Id of the clip to modify * @param status: target status of the clip This function creates an undo object and returns true on success */ static bool switchEnableState(const std::shared_ptr &timeline, int clipId); /* @brief change the clip state and accumulates for undo/redo */ static bool changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); static bool requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget); static bool requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget); static void setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack); static void enableMultitrackView(const std::shared_ptr &timeline, bool enable); - static void saveTimelineSelection(const std::shared_ptr &timeline, QList selection, const QDir &targetDir); + static void saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection, const QDir &targetDir); /** @brief returns the number of same type tracks between 2 tracks */ static int getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack); /** @brief returns an offset track id */ static int getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset); static QPair, QList> getAVTracksIds(const std::shared_ptr &timeline); }; #endif diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index a8225bfe2..cf736b064 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,613 +1,594 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelineitemmodel.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "bin/model/markerlistmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : TimelineModel(profile, std::move(undo_stack)) { } void TimelineItemModel::finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel) { ptr->weak_this_ = ptr; ptr->m_groups = std::make_unique(ptr); guideModel->registerSnapModel(ptr->m_snaps); } std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack) { std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack))); finishConstruct(ptr, std::move(guideModel)); return ptr; } TimelineItemModel::~TimelineItemModel() = default; QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const { READ_LOCK(); QModelIndex result; if (parent.isValid()) { auto trackId = int(parent.internalId()); Q_ASSERT(isTrack(trackId)); int clipId = getTrackById_const(trackId)->getClipByRow(row); if (clipId != -1) { result = createIndex(row, 0, quintptr(clipId)); } else if (row < getTrackClipsCount(trackId) + getTrackCompositionsCount(trackId)) { int compoId = getTrackById_const(trackId)->getCompositionByRow(row); if (compoId != -1) { result = createIndex(row, 0, quintptr(compoId)); } } else { // Invalid index requested Q_ASSERT(false); } } else if (row < getTracksCount() && row >= 0) { // Get sort order // row = getTracksCount() - 1 - row; auto it = m_allTracks.cbegin(); std::advance(it, row); int trackId = (*it)->getId(); result = createIndex(row, column, quintptr(trackId)); } return result; } /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const { return index(clipIndex, 0, index(trackIndex)); }*/ QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = m_allClips.at(clipId)->getCurrentTrackId(); if (trackId == -1) { // Clip is not inserted in a track qDebug() << "/// WARNING; INVALID CLIP INDEX REQUESTED\n________________"; return {}; } int row = getTrackById_const(trackId)->getRowfromClip(clipId); return index(row, 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); int trackId = m_allCompositions.at(compoId)->getCurrentTrackId(); return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const { // we retrieve iterator Q_ASSERT(m_iteratorTable.count(trackId) > 0); auto it = m_iteratorTable.at(trackId); int ind = (int)std::distance(m_allTracks.begin(), it); // Get sort order // ind = getTracksCount() - 1 - ind; return index(ind); } QModelIndex TimelineItemModel::parent(const QModelIndex &index) const { READ_LOCK(); // qDebug() << "TimelineItemModel::parent"<< index; if (index == QModelIndex()) { return index; } const int id = static_cast(index.internalId()); if (!index.isValid() || isTrack(id)) { return QModelIndex(); } if (isClip(id)) { const int trackId = getClipTrackId(id); return makeTrackIndexFromID(trackId); } if (isComposition(id)) { const int trackId = getCompositionTrackId(id); return makeTrackIndexFromID(trackId); } return {}; } int TimelineItemModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) { const int id = (int)parent.internalId(); if (!isTrack(id)) { // clips don't have children // if it is not a track, it is something invalid return 0; } return getTrackClipsCount(id) + getTrackCompositionsCount(id); } return getTracksCount(); } int TimelineItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QHash TimelineItemModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[ResourceRole] = "resource"; roles[ServiceRole] = "mlt_service"; roles[BinIdRole] = "binId"; roles[TrackIdRole] = "trackId"; roles[FakeTrackIdRole] = "fakeTrackId"; roles[FakePositionRole] = "fakePosition"; roles[StartRole] = "start"; roles[DurationRole] = "duration"; roles[MarkersRole] = "markers"; roles[KeyframesRole] = "keyframeModel"; roles[ShowKeyframesRole] = "showKeyframes"; roles[StatusRole] = "clipStatus"; roles[TypeRole] = "clipType"; roles[InPointRole] = "in"; roles[OutPointRole] = "out"; roles[FramerateRole] = "fps"; roles[GroupedRole] = "grouped"; roles[IsDisabledRole] = "disabled"; roles[IsAudioRole] = "audio"; roles[AudioLevelsRole] = "audioLevels"; roles[AudioChannelsRole] = "audioChannels"; roles[IsCompositeRole] = "composite"; roles[IsLockedRole] = "locked"; roles[FadeInRole] = "fadeIn"; roles[FadeOutRole] = "fadeOut"; roles[FileHashRole] = "hash"; roles[SpeedRole] = "speed"; roles[HeightRole] = "trackHeight"; roles[TrackTagRole] = "trackTag"; roles[ItemIdRole] = "item"; roles[ItemATrack] = "a_track"; roles[HasAudio] = "hasAudio"; roles[CanBeAudioRole] = "canBeAudio"; roles[CanBeVideoRole] = "canBeVideo"; roles[ReloadThumbRole] = "reloadThumb"; roles[ThumbsFormatRole] = "thumbsFormat"; roles[AudioRecordRole] = "audioRecord"; roles[EffectNamesRole] = "effectNames"; roles[EffectsEnabledRole] = "isStackEnabled"; roles[GrabbedRole] = "isGrabbed"; return roles; } QVariant TimelineItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!m_tractor || !index.isValid()) { // qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id); // Get data for a clip switch (role) { // TODO case NameRole: case Qt::DisplayRole: { QString result = clip->getProperty("kdenlive:clipname"); if (result.isEmpty()) { result = clip->getProperty("kdenlive:originalurl"); if (result.isEmpty()) { result = clip->getProperty("resource"); } if (!result.isEmpty()) { result = QFileInfo(result).fileName(); } else { result = clip->getProperty("mlt_service"); } } return result; } case ResourceRole: { QString result = clip->getProperty("resource"); if (result == QLatin1String("")) { result = clip->getProperty("mlt_service"); } return result; } case FakeTrackIdRole: return clip->getFakeTrackId(); case FakePositionRole: return clip->getFakePosition(); case BinIdRole: return clip->binId(); case TrackIdRole: return clip->getCurrentTrackId(); case ServiceRole: return clip->getProperty("mlt_service"); break; case AudioLevelsRole: return clip->getAudioWaveform(); case AudioChannelsRole: return clip->audioChannels(); case HasAudio: return clip->audioEnabled(); case IsAudioRole: return clip->isAudioOnly(); case CanBeAudioRole: return clip->canBeAudio(); case CanBeVideoRole: return clip->canBeVideo(); case MarkersRole: { return QVariant::fromValue(clip->getMarkerModel().get()); } case KeyframesRole: { return QVariant::fromValue(clip->getKeyframeModel()); } case StatusRole: return QVariant::fromValue(clip->clipState()); case TypeRole: return QVariant::fromValue(clip->clipType()); case StartRole: return clip->getPosition(); case DurationRole: return clip->getPlaytime(); - case GroupedRole: { - int parentId = m_groups->getDirectAncestor(id); - return parentId != -1 && parentId != m_temporarySelectionGroup; - } + case GroupedRole: + return m_groups->isInGroup(id); case EffectNamesRole: return clip->effectNames(); case InPointRole: return clip->getIn(); case OutPointRole: return clip->getOut(); case ShowKeyframesRole: return clip->showKeyframes(); case FadeInRole: return clip->fadeIn(); case FadeOutRole: return clip->fadeOut(); case ReloadThumbRole: return clip->forceThumbReload; case SpeedRole: return clip->getSpeed(); case GrabbedRole: return clip->isGrabbed(); default: break; } } else if (isTrack(id)) { // qDebug() << "DATA REQUESTED FOR TRACK "<< id; switch (role) { case NameRole: case Qt::DisplayRole: { return getTrackById_const(id)->getProperty("kdenlive:track_name").toString(); } case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Track); case DurationRole: // qDebug() << "DATA yielding duration" << m_tractor->get_playtime(); return getTrackById_const(id)->trackDuration(); case IsDisabledRole: // qDebug() << "DATA yielding mute" << 0; return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden(); case IsAudioRole: return getTrackById_const(id)->isAudioTrack(); case TrackTagRole: return getTrackTagById(id); case IsLockedRole: return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1; case HeightRole: { int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt(); if (collapsed > 0) { return collapsed; } int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt(); // qDebug() << "DATA yielding height" << height; return (height > 0 ? height : 60); } case ThumbsFormatRole: return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt(); case IsCompositeRole: { case AudioRecordRole: return getTrackById_const(id)->getProperty("kdenlive:audio_rec").toInt(); return Qt::Unchecked; } case EffectNamesRole: { return getTrackById_const(id)->effectNames(); } case EffectsEnabledRole: { return getTrackById_const(id)->stackEnabled(); } default: break; } } else if (isComposition(id)) { std::shared_ptr compo = m_allCompositions.at(id); switch (role) { case NameRole: case Qt::DisplayRole: case ResourceRole: case ServiceRole: return compo->displayName(); break; case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Composition); case StartRole: return compo->getPosition(); case TrackIdRole: return compo->getCurrentTrackId(); case DurationRole: return compo->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case InPointRole: return 0; case OutPointRole: return 100; case BinIdRole: return 5; case KeyframesRole: { return QVariant::fromValue(compo->getEffectKeyframeModel()); } case ShowKeyframesRole: return compo->showKeyframes(); case ItemATrack: return compo->getForcedTrack(); case MarkersRole: { QVariantList markersList; return markersList; } case GrabbedRole: return compo->isGrabbed(); default: break; } } else { qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role]; } return QVariant(); } void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value) { std::shared_ptr track = getTrackById(trackId); track->setProperty(name, value); QVector roles; if (name == QLatin1String("kdenlive:track_name")) { roles.push_back(NameRole); } else if (name == QLatin1String("kdenlive:locked_track")) { roles.push_back(IsLockedRole); } else if (name == QLatin1String("hide")) { roles.push_back(IsDisabledRole); if (!track->isAudioTrack()) { pCore->requestMonitorRefresh(); } } else if (name == QLatin1String("kdenlive:thumbs_format")) { roles.push_back(ThumbsFormatRole); } else if (name == QLatin1String("kdenlive:audio_rec")) { roles.push_back(AudioRecordRole); } if (!roles.isEmpty()) { QModelIndex ix = makeTrackIndexFromID(trackId); emit dataChanged(ix, ix, roles); } } void TimelineItemModel::setTrackStackEnabled(int tid, bool enable) { std::shared_ptr track = getTrackById(tid); track->setEffectStackEnabled(enable); QModelIndex ix = makeTrackIndexFromID(tid); emit dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole}); } void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr service) { std::shared_ptr track = getTrackById(tid); track->importEffects(std::move(service)); } QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const { return getTrackById_const(tid)->getProperty(name); } int TimelineItemModel::getFirstVideoTrackIndex() const { int trackId = -1; auto it = m_allTracks.cbegin(); while (it != m_allTracks.cend()) { trackId = (*it)->getId(); if (!(*it)->isAudioTrack()) { break; } ++it; } return trackId; } const QString TimelineItemModel::getTrackFullName(int tid) const { QString tag = getTrackTagById(tid); QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName; } const QString TimelineItemModel::groupsData() { return m_groups->toJson(); } bool TimelineItemModel::loadGroups(const QString &groupsData) { return m_groups->fromJson(groupsData); } -bool TimelineItemModel::isInMultiSelection(int cid) const -{ - if (m_temporarySelectionGroup == -1) { - return false; - } - bool res = (m_groups->getRootId(cid) == m_temporarySelectionGroup) && (m_groups->getDirectChildren(m_temporarySelectionGroup).size() != 1); - return res; -} - -bool TimelineItemModel::isSelected(int cid) const -{ - if (m_temporarySelectionGroup == -1) { - return false; - } - return m_groups->getRootId(cid) == m_temporarySelectionGroup; -} - void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) { QVector roles; if (start) { roles.push_back(TimelineModel::StartRole); if (updateThumb) { roles.push_back(TimelineModel::InPointRole); } } if (duration) { roles.push_back(TimelineModel::DurationRole); if (updateThumb) { roles.push_back(TimelineModel::OutPointRole); } } emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) { emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::buildTrackCompositing(bool rebuild) { auto it = m_allTracks.cbegin(); QScopedPointer field(m_tractor->field()); field->lock(); // Make sure all previous track compositing is removed if (rebuild) { QScopedPointer service(new Mlt::Service(field->get_service())); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } } QString composite = TransitionsRepository::get()->getCompositingTransition(); while (it != m_allTracks.cend()) { int trackId = getTrackMltIndex((*it)->getId()); if (!composite.isEmpty() && !(*it)->isAudioTrack()) { // video track, add composition std::unique_ptr transition = TransitionsRepository::get()->getTransition(composite); transition->set("internal_added", 237); transition->set("always_active", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } else if ((*it)->isAudioTrack()) { // audio mix std::unique_ptr transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); transition->set("internal_added", 237); transition->set("always_active", 1); transition->set("sum", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } ++it; } field->unlock(); if (composite.isEmpty()) { pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage); } } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) { emit dataChanged(topleft, bottomright, {role}); } void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k) { // qDebug()<<"FORWARDING beginRemoveRows"<. * ***************************************************************************/ #ifndef TIMELINEITEMMODEL_H #define TIMELINEITEMMODEL_H #include "timelinemodel.hpp" #include "undohelper.hpp" /* @brief This class is the thin wrapper around the TimelineModel that provides interface for the QML. It derives from AbstractItemModel to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been chosen because it is consistent with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class MarkerListModel; class TimelineItemModel : public TimelineModel { Q_OBJECT public: /* @brief construct a timeline object and returns a pointer to the created object @param undo_stack is a weak pointer to the undo stack of the project @param guideModel ptr to the guide model of the project */ static std::shared_ptr construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack); friend bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor); protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: ~TimelineItemModel() override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; // QModelIndex makeIndex(int trackIndex, int clipIndex) const; /* @brief Creates an index based on the ID of the clip*/ QModelIndex makeClipIndexFromID(int clipId) const override; /* @brief Creates an index based on the ID of the compoition*/ QModelIndex makeCompositionIndexFromID(int compoId) const override; /* @brief Creates an index based on the ID of the track*/ QModelIndex makeTrackIndexFromID(int trackId) const override; QModelIndex parent(const QModelIndex &index) const override; Q_INVOKABLE void setTrackProperty(int tid, const QString &name, const QString &value); /* @brief Enabled/disabled a track's effect stack */ Q_INVOKABLE void setTrackStackEnabled(int tid, bool enable); Q_INVOKABLE QVariant getTrackProperty(int tid, const QString &name) const; /** @brief returns the lower video track index in timeline. **/ int getFirstVideoTrackIndex() const; const QString getTrackFullName(int tid) const; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) override; /** @brief Rebuild track compositing */ void buildTrackCompositing(bool rebuild = false); /** @brief Import track effects */ void importTrackEffects(int tid, std::weak_ptr service); const QString groupsData(); bool loadGroups(const QString &groupsData); - /* @brief returns true if clip is in temporary selection group. - */ - bool isInMultiSelection(int cid) const; - bool isSelected(int cid) const; void _beginRemoveRows(const QModelIndex &, int, int) override; void _beginInsertRows(const QModelIndex &, int, int) override; void _endRemoveRows() override; void _endInsertRows() override; void _resetView() override; protected: // This is an helper function that finishes a construction of a freshly created TimelineItemModel static void finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel); }; #endif diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index 51220d560..42242bd70 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,2845 +1,2932 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))( parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))( parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) .method("requestItemResize", select_overload(&TimelineModel::requestItemResize))( parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( parameter_names("itemIds", "logUndo", "type")) .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo")) .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))( parameter_names("pos", "id", "trackName", "audioTrack")) - .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")); + .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")) + .method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion")) + .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear")) + .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId")) + .method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("ids")); } int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_tractor(new Mlt::Tractor(*profile)) , m_snaps(new SnapModel()) , m_undoStack(std::move(undo_stack)) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) - , m_temporarySelectionGroup(-1) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_blockRefresh(false) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); m_blackClip->set("set.test_audio", 0); m_blackClip->set("length", INT_MAX); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.begin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.end(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : (*it)->getId(); } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; if (it != m_allTracks.end()) { ++it; } while (it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } ++it; } if (it != m_allTracks.end() && !(*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; if (it != m_allTracks.begin()) { --it; } while (it != m_allTracks.begin()) { if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } --it; } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView; if (trackId == -1) { return false; } Q_ASSERT(isClip(clipId)); if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; bool localUpdateView = updateView; // qDebug()<<"MOVING CLIP FROM: "< 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = fakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool allowViewUpdate) { Q_UNUSED(allowViewUpdate); QWriteLocker locker(&m_lock); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = sourceTrackId; } if (currentPos == position && sourceTrackId == trackId) { return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { return position; } } int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { return currentPos; } possible = requestClipMove(clipId, trackId, position, true, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check space after the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1; if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); possible = requestClipMove(clipId, trackId, updatedPos, true, false, false); if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (getTrackById_const(trackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = currentTrack; } if (currentPos == position && currentTrack == trackId) { return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { return position; } /*bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position;*/ return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug() << " / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state; return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, 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; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { QWriteLocker locker(&m_lock); TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); int target_track = audioDrop ? m_videoTarget : m_audioTarget; qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); int mirror = getMirrorTrackId(trackId); bool canMirrorDrop = !useTargets && mirror > -1; if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = mirror; } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(clipId)) { return requestGroupDeletion(clipId, undo, redo); } return requestClipDeletion(clipId, undo, redo); } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { bool res = requestGroupDeletion(itemId, logUndo); TRACE_RES(res); + requestClearSelection(true); return res; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = false; QString actionLabel; if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); res = requestClipDeletion(itemId, undo, redo); } else { actionLabel = i18n("Delete Composition"); res = requestCompositionDeletion(itemId, undo, redo); } if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } TRACE_RES(res); + requestClearSelection(true); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { - emit removeFromSelection(clipId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { - emit removeFromSelection(compositionId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { if (track->isLocked()) { continue; } std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); Q_ASSERT(isClip(clipId)); if (getGroupElements(groupId).count(clipId) == 0) { // this group doesn't contain the clip, abort return false; } bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view, and compositions from top to bottom std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_track](int clipId1, int clipId2) { int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move bool updatePositionOnly = false; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; }; } // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } // Reverse sort. We need to insert from left to right to avoid confusing the view std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; bool updateThisView = allowViewRefresh; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, logUndo); + requestClearSelection(); if (!m_groups->isInGroup(clipId)) { TRACE_RES(false); return false; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestGroupDeletion(clipId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Remove group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); - if (m_temporarySelectionGroup == current_group) { - m_temporarySelectionGroup = -1; + if (m_currentSelection == current_group) { + m_currentSelection = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); auto items = m_groups->getLeaves(groupId); for (int id : items) { if (id == itemId) { all_items.insert(id); continue; } int start = getItemPosition(id); int end = in + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; for (int id : all_items) { result = result && requestItemResize(id, size, right, logUndo, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } int res = result ? size : -1; TRACE_RES(res); return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (getItemTrackId(itemId) != -1) { qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++"; QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); TRACE(ids, logUndo, type); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - if (m_temporarySelectionGroup > -1) { - m_groups->destructGroupItem(m_temporarySelectionGroup); - // We don't log in undo the selection changes - // int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin(); - // requestClipUngroup(firstChild, undo, redo); - m_temporarySelectionGroup = -1; - } int result = requestClipsGroup(ids, undo, redo, type); - if (type == GroupType::Selection) { - m_temporarySelectionGroup = result; - } if (result > -1 && logUndo && type != GroupType::Selection) { PUSH_UNDO(undo, redo, i18n("Group clips")); } TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { QWriteLocker locker(&m_lock); for (int id : ids) { if (isClip(id)) { if (getClipTrackId(id) == -1) { return -1; } } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } if (type == GroupType::Selection && ids.size() == 1) { // only one element selected, no group created return -1; } int groupId = m_groups->groupItems(ids, undo, redo, type); return groupId; } bool TimelineModel::requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemIds, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; - int old_selection = m_temporarySelectionGroup; - if (m_temporarySelectionGroup != -1) { - // Delete selection group without undo - Fun tmp_undo = []() { return true; }; - Fun tmp_redo = []() { return true; }; - requestClipUngroup(m_temporarySelectionGroup, tmp_undo, tmp_redo); - m_temporarySelectionGroup = -1; - } + requestClearSelection(); std::unordered_set roots; - for (int itemId : itemIds) { - int root = m_groups->getRootId(itemId); - if (root != old_selection) { - roots.insert(root); - } - } + std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); for (int root : roots) { result = result && requestClipUngroup(root, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); + requestClearSelection(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; - if (itemId == m_temporarySelectionGroup) { - // Delete selection group without undo - Fun tmp_undo = []() { return true; }; - Fun tmp_redo = []() { return true; }; - requestClipUngroup(itemId, tmp_undo, tmp_redo); - m_temporarySelectionGroup = -1; - } else { - result = requestClipUngroup(itemId, undo, redo); - } + result = requestClipUngroup(itemId, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); return m_groups->ungroupItem(itemId, undo, redo); } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, updateView); return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track QWriteLocker locker(&m_lock); TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView) { // qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; if (reloadView) { // don't reload view on each track load on project opening _resetView(); } } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { // qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::requestBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::requestNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::requestPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, std::move(transProps)); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); notifyChange(modelIndex, modelIndex, StartRole); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.emplace_back(trackPos, compo.first); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count()); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); notifyChange(modelIndex, modelIndex, ItemATrack); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } if (!clip->checkConsistency()) { qDebug() << "Consistency check failed for clip" << cp.first; return false; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { auto tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } + + // Check that the selection is in a valid state: + if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isGroup(m_currentSelection)) { + qDebug() << "Selection is in inconsistent state"; + return false; + } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } std::shared_ptr TimelineModel::producer() { return std::make_shared(tractor()); } void TimelineModel::checkRefresh(int start, int end) { if (m_blockRefresh) { return; } int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo); } if (old_trackId != -1) { m_allClips[clipId]->refreshProducerFromBin(); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); } else { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); return false; } } else { // If clip is not inserted on a track, we just change the producer m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.begin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.begin() ? trackId : (*it)->getId(); } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.end()) { ++it; if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.end() ? trackId : (*it)->getId(); } + +void TimelineModel::requestClearSelection(bool onDeletion) +{ + QWriteLocker locker(&m_lock); + TRACE(); + if (m_currentSelection == -1) { + return; + } + if (isGroup(m_currentSelection)) { + if (m_groups->getType(m_currentSelection) == GroupType::Selection) { + m_groups->destructGroupItem(m_currentSelection); + } + } else { + Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection)); + } + m_currentSelection = -1; + emit selectionChanged(); +} +void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo) +{ + Fun operation = [this, onDeletion]() { + requestClearSelection(onDeletion); + return true; + }; + Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); }; + if (operation()) { + UPDATE_UNDO_REDO(operation, reverse, undo, redo); + } +} + +std::unordered_set TimelineModel::getCurrentSelection() const +{ + READ_LOCK(); + if (m_currentSelection == -1) { + return {}; + } + if (isGroup(m_currentSelection)) { + return m_groups->getLeaves(m_currentSelection); + } else { + Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection)); + return {m_currentSelection}; + } +} +void TimelineModel::requestAddToSelection(int itemId, bool clear) +{ + QWriteLocker locker(&m_lock); + TRACE(itemId, clear); + if (clear) { + requestClearSelection(); + } + std::unordered_set selection = getCurrentSelection(); + if (selection.count(itemId) == 0) { + selection.insert(itemId); + requestSetSelection(selection); + } +} + +void TimelineModel::requestRemoveFromSelection(int itemId) +{ + QWriteLocker locker(&m_lock); + TRACE(itemId); + std::unordered_set selection = getCurrentSelection(); + if (selection.count(itemId) > 0) { + selection.erase(itemId); + requestSetSelection(selection); + } +} + +bool TimelineModel::requestSetSelection(const std::unordered_set &ids) +{ + QWriteLocker locker(&m_lock); + TRACE(ids); + + requestClearSelection(); + // if the items are in groups, we must retrieve their topmost containing groups + std::unordered_set roots; + std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); + + bool result = true; + if (roots.size() == 0) { + m_currentSelection = -1; + } else if (roots.size() == 1) { + m_currentSelection = *(roots.begin()); + } else { + Fun undo = []() { return true; }; + Fun redo = []() { return true; }; + result = m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection); + Q_ASSERT(m_currentSelection >= 0); + } + emit selectionChanged(); + return result; +} + +bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo) +{ + Fun reverse = [this]() { + requestClearSelection(false); + return true; + }; + Fun operation = [this, ids]() { return requestSetSelection(ids); }; + if (operation()) { + UPDATE_UNDO_REDO(operation, reverse, undo, redo); + return true; + } + return false; +} diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index 102f3e072..aa61d4b7c 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,754 +1,780 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINEMODEL_H #define TIMELINEMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include #include #include #include class AssetParameterModel; class EffectStackModel; class ClipModel; class CompositionModel; class DocUndoStack; class GroupsModel; class SnapModel; class TimelineItemModel; class TrackModel; /* @brief This class represents a Timeline object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications. This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in any interactions with the objects and have nothing to do with Melt IDs. This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the version in timeline. This is also required to generate the Undo/Redo operators The undo/redo system is designed around lambda functions. Each time a function executes an elementary change to the model, it writes the corresponding operation and its reverse, respectively in the redo and the undo lambdas. This way, if an operation fails for some reason, we can easily cancel the steps that have been done so far without corrupting anything. The other advantage is that operations are easy to compose, and you get a undo/redo pair for free no matter in which way you combine them. Most of the modification functions are named requestObjectAction. Eg, if the object is a clip and we want to move it, we call requestClipMove. These functions always return a bool indicating success, and when they return false they should guarantee than nothing has been modified. Most of the time, these functions come in two versions: the first one is the entry point if you want to perform only the action (and not compose it with other actions). This version will generally automatically push and Undo object on the Application stack, in case the user later wants to cancel the operation. It also generally goes the extra mile to ensure the operation is done in a way that match the user's expectation: for example requestClipMove checks whether the clip belongs to a group and in that case actually mouves the full group. The other version of the function, if it exists, is intended for composition (using the action as part of a complex operation). It takes as input the undo/redo lambda corresponding to the action that is being performed and accumulates on them. Note that this version does the minimal job: in the example of the requestClipMove, it will not move the full group if the clip is in a group. Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it. We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far. For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good, otherwise we revert what we've already done. This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost. It derives from AbstractItemModel (indirectly through TimelineItemModel) to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been chosen because it is consistent with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class TimelineModel : public QAbstractItemModel_shared_from_this { Q_OBJECT protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: friend class TrackModel; template friend class MoveableItem; friend class ClipModel; friend class CompositionModel; friend class GroupsModel; friend class TimelineController; friend struct TimelineFunctions; /// Two level model: tracks and clips on track enum { NameRole = Qt::UserRole + 1, ResourceRole, /// clip only ServiceRole, /// clip only StartRole, /// clip only BinIdRole, /// clip only TrackIdRole, FakeTrackIdRole, FakePositionRole, MarkersRole, /// clip only StatusRole, /// clip only TypeRole, /// clip only KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only CanBeAudioRole, /// clip only CanBeVideoRole, /// clip only IsDisabledRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only AudioChannelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only TrackTagRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only ItemATrack, /// composition only ItemIdRole, ThumbsFormatRole, /// track only EffectNamesRole, // track and clip only EffectsEnabledRole, // track and clip only GrabbedRole, /// clip+composition only AudioRecordRole /// track only }; ~TimelineModel() override; Mlt::Tractor *tractor() const { return m_tractor.get(); } /* @brief Load tracks from the current tractor, used on project opening */ void loadTractor(); /* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc */ std::shared_ptr producer(); Mlt::Profile *getProfile(); /* @brief returns the number of tracks */ int getTracksCount() const; /* @brief returns the track index (id) from its position */ int getTrackIndexFromPosition(int pos) const; /* @brief returns the track index (id) from its position */ Q_INVOKABLE bool isAudioTrack(int trackId) const; /* @brief returns the number of clips */ int getClipsCount() const; /* @brief returns the number of compositions */ int getCompositionsCount() const; /* @brief Returns the id of the track containing clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipTrackId(int clipId) const; /* @brief Returns the id of the track containing composition (-1 if it is not inserted) @param clipId Id of the composition to test */ Q_INVOKABLE int getCompositionTrackId(int compoId) const; /* @brief Convenience function that calls either of the previous ones based on item type*/ Q_INVOKABLE int getItemTrackId(int itemId) const; Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ Q_INVOKABLE int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* Returns the current speed of a clip */ double getClipSpeed(int clipId) const; /* @brief Helper function to query the amount of free space around a clip * @param clipId: the queried clip. If it is not inserted on a track, this functions returns 0 * @param after: if true, we return the blank after the clip, otherwise, before. */ int getBlankSizeNearClip(int clipId, bool after) const; /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getClipSplitPartner(int clipId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isTrack(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a group */ bool isGroup(int id) const; /* @brief Given a composition Id, returns its underlying parameter model */ std::shared_ptr getCompositionParameterModel(int compoId) const; /* @brief Given a clip Id, returns its underlying effect stack model */ std::shared_ptr getClipEffectStackModel(int clipId) const; /* @brief Returns the position of clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipPosition(int clipId) const; Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId, bool notify = true); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); Q_INVOKABLE bool copyTrackEffect(int trackId, const QString &sourceId); bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration); /* @brief Returns the closest snap point within snapDistance */ Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance); /** @brief Return the previous track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getPreviousTrackId(int trackId); /** @brief Return the next track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getNextTrackId(int trackId); /* @brief Returns the in cut position of a clip @param clipId Id of the clip to test */ int getClipIn(int clipId) const; /* @brief Returns the clip state (audio/video only) */ PlaylistState::ClipState getClipState(int clipId) const; /* @brief Returns the bin id of the clip master @param clipId Id of the clip to test */ const QString getClipBinId(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ int getClipPlaytime(int clipId) const; /* @brief Returns the size of the clip's frame (widthxheight) @param clipId Id of the clip to test */ QSize getClipFrameSize(int clipId) const; /* @brief Returns the number of clips in a given track @param trackId Id of the track to test */ int getTrackClipsCount(int trackId) const; /* @brief Returns the number of compositions in a given track @param trackId Id of the track to test */ int getTrackCompositionsCount(int trackId) const; /* @brief Returns the position of the track in the order of the tracks @param trackId Id of the track to test */ int getTrackPosition(int trackId) const; /* @brief Returns the track's index in terms of mlt's internal representation */ int getTrackMltIndex(int trackId) const; /* @brief Returns a sort position for tracks. * @param separated: if true, the tracks will be sorted like: V2,V1,A1,A2 * Otherwise, the tracks will be sorted like V2,A2,V1,A1 */ int getTrackSortValue(int trackId, bool separated) const; /* @brief Returns the ids of the tracks below the given track in the order of the tracks Returns an empty list if no track available @param trackId Id of the track to test */ QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const; /* @brief Returns the MLT track index of the video track just below the given track @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Returns the Track id of the video track just below the given track @param trackId Id of the track to test */ int getPreviousVideoTrackIndex(int trackId) const; /* @brief Returns the Id of the corresponding audio track. If trackId corresponds to video1, this will return audio 1 and so on */ int getMirrorAudioTrackId(int trackId) const; int getMirrorVideoTrackId(int trackId) const; int getMirrorTrackId(int trackId) const; /* @brief Move a clip to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param clipId is the ID of the clip @param trackId is the ID of the target track @param position is the position where we want to move @param updateView if set to false, no signal is sent to qml @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false); /* @brief Move a composition to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param transid is the ID of the composition @param trackId is the ID of the track */ Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true); /* Same function, but accumulates undo and redo, and doesn't check for group*/ bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* When timeline edit mode is insert or overwrite, we fake the move (as it will overlap existing clips, and only process the real move on drop */ bool fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @brief Given an intended move, try to suggest a more valid one (accounting for snaps and missing UI calls) @param clipId id of the clip to move @param trackId id of the target track @param position target position @param snapDistance the maximum distance for a snap result, -1 for no snapping of the clip @param dontRefreshMasterClip when false, no view refresh is attempted */ Q_INVOKABLE int suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance = -1); Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance = -1, bool allowViewUpdate = true); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance = -1); /* @brief Request clip insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param binClipId id of the clip in the bin @param track Id of the track where to insert @param position Requested position @param ID return parameter of the id of the inserted clip @param logUndo if set to false, no undo object is stored @param refreshView whether the view should be refreshed @param useTargets: if true, the Audio/video split will occur on the set targets. Otherwise, they will be computed as an offset from the middle line */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false, bool useTargets = true); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo); /* @brief Creates a new clip instance without inserting it. This action is undoable, returns true on success @param binClipId: Bin id of the clip to insert @param id: return parameter for the id of the newly created clip. @param state: The desired clip state (original, audio/video only). */ bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo); /* @brief Deletes the given clip or composition from the timeline This action is undoable Returns true on success. If it fails, nothing is modified. If the clip/composition is in a group, the call is deferred to requestGroupDeletion @param clipId is the ID of the clip/composition @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestItemDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Move a group to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clips in the group are not in inserted in a track yet, they get inserted for the first time. @param clipId is the id of the clip that triggers the group move @param groupId is the id of the group @param delta_track is the delta applied to the track index @param delta_pos is the requested position change @param updateView if set to false, no signal is sent to qml for the clip clipId @param logUndo if set to true, an undo object is created @param allowViewRefresh if false, the view will never get updated (useful for suggestMove) */ bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @brief Deletes all clips inside the group that contains the given clip. This action is undoable Note that if their is a hierarchy of groups, all of them will be deleted. Returns true on success. If it fails, nothing is modified. @param clipId is the id of the clip that triggers the group deletion */ Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true); bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns the real size reached (can be different, if snapping occurs). If it fails, nothing is modified, and -1 is returned @param itemId is the ID of the item @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param logUndo if set to true, an undo object is created @param snap if set to true, the resize order will be coerced to use the snapping grid */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* Same function, but accumulates undo and redo and doesn't deal with snapping*/ bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false); /* @brief Group together a set of ids The ids are either a group ids or clip ids. The involved clip must already be inserted in a track This action is undoable Returns the group id on success, -1 if it fails and nothing is modified. Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well. @param ids Set of ids to group */ int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal); int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal); /* @brief Destruct the topmost group containing clip This action is undoable Returns true on success. If it fails, nothing is modified. @param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well) */ bool requestClipUngroup(int itemId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestClipUngroup(int itemId, Fun &undo, Fun &redo); // convenience functions for several ids at the same time bool requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo = true); /* @brief Create a track at given position This action is undoable Returns true on success. If it fails, nothing is modified. @param Requested position (order). If set to -1, the track is inserted last. @param id is a return parameter that holds the id of the resulting track (-1 on failure) */ bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false); /* Same function, but accumulates undo and redo*/ bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true); /* @brief Delete track with given id This also deletes all the clips contained in the track. This action is undoable Returns true on success. If it fails, nothing is modified. @param trackId id of the track to delete */ bool requestTrackDeletion(int trackId); /* Same function, but accumulates undo and redo*/ bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo); /* @brief Get project duration Returns the duration in frames */ int duration() const; static int seekDuration; // Duration after project end where seeking is allowed /* @brief Get all the elements of the same group as the given clip. If there is a group hierarchy, only the topmost group is considered. @param clipId id of the clip to test */ std::unordered_set getGroupElements(int clipId); /* @brief Removes all the elements on the timeline (tracks and clips) */ bool requestReset(Fun &undo, Fun &redo); /* @brief Updates the current the pointer to the current undo_stack Must be called for example when the doc change */ void setUndoStack(std::weak_ptr undo_stack); /* @brief Requests the best snapped position for a clip @param pos is the clip's requested position @param length is the clip's duration @param pts snap points to ignore (for example currently moved clip) @param snapDistance the maximum distance for a snap result, -1 for no snapping @returns best snap position or -1 if no snap point is near */ int requestBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int cursorPosition = 0, int snapDistance = -1); /* @brief Requests the next snapped point @param pos is the current position */ int requestNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int requestPreviousSnapPos(int pos); /* @brief Add a new snap point @param pos is the current position */ void addSnap(int pos); /* @brief Remove snap point @param pos is the current position */ void removeSnap(int pos); /* @brief Request composition insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param transitionId Identifier of the Mlt transition to insert (as given by repository) @param track Id of the track where to insert @param position Requested position @param length Requested initial length. @param id return parameter of the id of the inserted composition @param logUndo if set to false, no undo object is stored */ bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove = false); /* @brief This function change the global (timeline-wise) enabled state of the effects It disables/enables track and clip effects (recursively) */ void setTimelineEffectsEnabled(bool enabled); /* @brief Get a timeline clip id by its position or -1 if not found */ int getClipByPosition(int trackId, int position) const; /* @brief Get a timeline composition id by its starting position or -1 if not found */ int getCompositionByPosition(int trackId, int position) const; /* @brief Returns a list of all items that are intersect with a given range. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param start is the position where we the items should start * @param end is the position after which items will not be selected, set to -1 to get all clips on track * @param listCompositions if enabled, the list will also contains composition ids */ std::unordered_set getItemsInRange(int trackId, int start, int end = -1, bool listCompositions = true); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Inform asset view of duration change */ virtual void adjustAssetRange(int clipId, int in, int out); void requestClipReload(int clipId); void requestClipUpdate(int clipId, const QVector &roles); /** @brief define current edit mode (normal, insert, overwrite */ void setEditMode(TimelineMode::EditMode mode); Q_INVOKABLE bool normalEdit() const; /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); std::shared_ptr getTrackEffectStackModel(int trackId); /** @brief Add slowmotion effect to clip in timeline. @param clipId id of the target clip @param speed: speed in percentage. 100 corresponds to original speed, 50 to half the speed This functions create an undo object and also apply the effect to the corresponding audio if there is any. Returns true on success, false otherwise (and nothing is modified) */ bool requestClipTimeWarp(int clipId, double speed); /* @brief Same function as above, but doesn't check for paired audio and accumulate undo/redo */ bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo); void replugClip(int clipId); /** @brief Refresh the tractor profile in case a change was requested. */ void updateProfile(Mlt::Profile *profile); + /** @brief Clear the current selection + @param onDeletion is true when the selection is cleared as a result of a deletion + */ + Q_INVOKABLE void requestClearSelection(bool onDeletion = false); + // same function with undo/redo accumulation + void requestClearSelection(bool onDeletion, Fun &undo, Fun &redo); + + /** @brief Add the given item to the selection + If @param clear is true, the selection is first cleared + */ + Q_INVOKABLE void requestAddToSelection(int itemId, bool clear = false); + + /** @brief Remove the given item from the selection */ + Q_INVOKABLE void requestRemoveFromSelection(int itemId); + + /** @brief Set the selection to the set of given ids */ + bool requestSetSelection(const std::unordered_set &ids); + // same function with undo/redo + bool requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo); + + /** @brief Returns a set containing all the items in the selection */ + std::unordered_set getCurrentSelection() const; + protected: /* @brief Register a new track. This is a call-back meant to be called from TrackModel @param pos indicates the number of the track we are adding. If this is -1, then we add at the end. */ void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip, bool registerProducer = false); /* @brief Register a new composition. This is a call-back meant to be called from CompositionModel */ void registerComposition(const std::shared_ptr &composition); /* @brief Register a new group. This is a call-back meant to be called from GroupsModel */ void registerGroup(int groupId); /* @brief Deregister and destruct the track with given id. @parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor */ Fun deregisterTrack_lambda(int id, bool updateView = false); /* @brief Return a lambda that deregisters and destructs the clip with given id. Note that the clip must already be deleted from its track and groups. */ Fun deregisterClip_lambda(int id); /* @brief Return a lambda that deregisters and destructs the composition with given id. */ Fun deregisterComposition_lambda(int compoId); /* @brief Deregister a group with given id */ void deregisterGroup(int id); /* @brief Helper function to get a pointer to the track, given its id */ std::shared_ptr getTrackById(int trackId); const std::shared_ptr getTrackById_const(int trackId) const; /*@brief Helper function to get a pointer to a clip, given its id*/ std::shared_ptr getClipPtr(int clipId) const; /*@brief Helper function to get a pointer to a composition, given its id*/ std::shared_ptr getCompositionPtr(int compoId) const; /* @brief Returns next valid unique id to create an object */ static int getNextId(); /* @brief unplant and the replant all the compositions in the correct order @param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1 */ bool replantCompositions(int currentCompo, bool updateView); /* @brief Unplant the composition with given Id */ bool unplantComposition(int compoId); /* Same function but accumulates undo and redo, and doesn't check for group*/ bool requestClipDeletion(int clipId, Fun &undo, Fun &redo); bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo); /** @brief Check tracks duration and update black track accordingly */ void updateDuration(); /** @brief Get a track tag (A1, V1, V2,...) through its id */ const QString getTrackTagById(int trackId) const; /** @brief Attempt to make a clip move without ever updating the view */ bool requestClipMoveAttempt(int clipId, int trackId, int position); public: /* @brief Debugging function that checks consistency with Mlt objects */ bool checkConsistency(); protected: /* @brief Refresh project monitor if cursor was inside range */ void checkRefresh(int start, int end); /* @brief Send signal to require clearing effet/composition view */ void clearAssetView(int itemId); bool m_blockRefresh; signals: /* @brief signal triggered by clearAssetView */ void requestClearAssetView(int); void requestMonitorRefresh(); /* @brief signal triggered by track operations */ void invalidateZone(int in, int out); /* @brief signal triggered when a track duration changed (insertion/deletion) */ void durationUpdated(); - /* @brief an item was deleted, make sure it is removed from selection */ - void removeFromSelection(int id); + + /* @brief Signal sent whenever the selection changes */ + void selectionChanged(); protected: std::unique_ptr m_tractor; std::list> m_allTracks; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id. std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers std::unordered_map> m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers static int next_id; // next valid id to assign std::unique_ptr m_groups; std::shared_ptr m_snaps; std::unordered_set m_allGroups; // ids of all the groups std::weak_ptr m_undoStack; Mlt::Profile *m_profile; // The black track producer. Its length / out should always be adjusted to the projects's length std::unique_ptr m_blackClip; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself - // id of the currently selected group in timeline, should be destroyed on each new selection - int m_temporarySelectionGroup; + // id of the selection. If -1, there is no selection, if positive, then it might either be the id of the selection group, or the id of an individual + // item, or, finally, the id of a group which is not of type selection. The last case happens when the selection exactly matches an existing group + // (in that case we cannot further group it because the selection would have only one child, which is prohibited by design) + int m_currentSelection = -1; // The index of the temporary overlay track in tractor, or -1 if not connected int m_overlayTrackCount; // The preferred audio target for clip insertion or -1 if not defined int m_audioTarget; // The preferred video target for clip insertion or -1 if not defined int m_videoTarget; // Timeline editing mode TimelineMode::EditMode m_editMode; // what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel protected: virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _beginInsertRows(const QModelIndex &, int, int) = 0; virtual void _endRemoveRows() = 0; virtual void _endInsertRows() = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml index f677e5649..df23d3518 100644 --- a/src/timeline2/view/qml/Clip.qml +++ b/src/timeline2/view/qml/Clip.qml @@ -1,759 +1,759 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * 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. * * 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 . */ import QtQuick 2.6 import QtQuick.Controls 2.2 import Kdenlive.Controls 1.0 import QtQml.Models 2.2 import QtQuick.Window 2.2 import 'Timeline.js' as Logic import com.enums 1.0 Rectangle { id: clipRoot property real timeScale: 1.0 property string clipName: '' property string clipResource: '' property string mltService: '' property string effectNames property int modelStart property real scrollX: 0 property int inPoint: 0 property int outPoint: 0 property int clipDuration: 0 property bool isAudio: false property int audioChannels property bool showKeyframes: false property bool isGrabbed: false property bool grouped: false property var audioLevels property var markers property var keyframeModel property int clipStatus: 0 property int itemType: 0 property int fadeIn: 0 property int fadeOut: 0 property int binId: 0 property var parentTrack property int trackIndex //Index in track repeater property int clipId //Id of the clip in the model property int trackId: -1 // Id of the parent track in the model property int fakeTid: -1 property int fakePosition: 0 property int originalTrackId: -1 property int originalX: x property int originalDuration: clipDuration property int lastValidDuration: clipDuration property int draggedX: x property bool selected: false property bool isLocked: parentTrack && parentTrack.isLocked == true property bool hasAudio property bool canBeAudio property bool canBeVideo property string hash: 'ccc' //TODO property double speed: 1.0 property color borderColor: 'black' property bool forceReloadThumb: false width : clipDuration * timeScale; opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0 signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim) signal trimmedIn(var clip, bool shiftTrim) signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim) signal trimmedOut(var clip, bool shiftTrim) onIsGrabbedChanged: { if (clipRoot.isGrabbed) { clipRoot.forceActiveFocus(); mouseArea.focus = true } } onInPointChanged: { if (parentTrack && parentTrack.isAudio) { thumbsLoader.item.reload() } } onClipResourceChanged: { if (itemType == ProducerType.Color) { color: Qt.darker(getColor()) } } ToolTip { visible: mouseArea.containsMouse && !dragProxyArea.pressed font.pixelSize: root.baseUnit delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text text: clipRoot.clipName + ' (' + timeline.timecode(clipRoot.inPoint) + '-' + timeline.timecode(clipRoot.outPoint) + ')' } } onKeyframeModelChanged: { console.log('keyframe model changed............') if (effectRow.keyframecanvas) { effectRow.keyframecanvas.requestPaint() } } onClipDurationChanged: { width = clipDuration * timeScale; } onModelStartChanged: { x = modelStart * timeScale; } onFakePositionChanged: { x = fakePosition * timeScale; } onFakeTidChanged: { if (clipRoot.fakeTid > -1 && parentTrack) { if (clipRoot.parent != dragContainer) { var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y); clipRoot.parent = dragContainer pos = clipRoot.mapFromGlobal(pos.x, pos.y) clipRoot.x = pos.x clipRoot.y = pos.y } clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y } } onForceReloadThumbChanged: { // TODO: find a way to force reload of clip thumbs if (thumbsLoader.item) { thumbsLoader.item.reload() } } onTimeScaleChanged: { x = modelStart * timeScale; width = clipDuration * timeScale; labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 if (parentTrack && parentTrack.isAudio) { thumbsLoader.item.reload(); } } onScrollXChanged: { labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } border.color: selected? activePalette.highlight : grouped ? root.groupColor : borderColor border.width: isGrabbed ? 8 : 1.5 function updateDrag() { var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height) initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false) } function getColor() { if (clipStatus == ClipState.Disabled) { return 'grey' } if (itemType == ProducerType.Color) { var color = clipResource.substring(clipResource.length - 9) if (color[0] == '#') { return color } return '#' + color.substring(color.length - 8, color.length - 2) } return isAudio? root.audioColor : root.videoColor } /* function reparent(track) { console.log('TrackId: ',trackId) parent = track height = track.height parentTrack = track trackId = parentTrack.trackId console.log('Reparenting clip to Track: ', trackId) //generateWaveform() } */ property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '') property bool isImage: itemType == ProducerType.Image property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + (isImage ? '#0' : '#') property string inThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(inPoint * speed) property string outThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(outPoint * speed) DropArea { //Drop area for clips anchors.fill: clipRoot keys: 'kdenlive/effect' property string dropData property string dropSource property int dropRow: -1 onEntered: { dropData = drag.getDataAsString('kdenlive/effect') dropSource = drag.getDataAsString('kdenlive/effectsource') } onDropped: { console.log("Add effect: ", dropData) if (dropSource == '') { // drop from effects list controller.addClipEffect(clipRoot.clipId, dropData); } else { controller.copyClipEffect(clipRoot.clipId, dropSource); } dropSource = '' dropRow = -1 drag.acceptProposedAction } } onAudioLevelsChanged: { if (parentTrack && parentTrack.isAudio && thumbsLoader.item) { thumbsLoader.item.reload() } } MouseArea { id: mouseArea visible: root.activeTool === 0 anchors.fill: clipRoot acceptedButtons: Qt.RightButton hoverEnabled: true cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { root.stopScrolling = true if (mouse.button == Qt.RightButton) { if (timeline.selection.indexOf(clipRoot.clipId) == -1) { - timeline.addSelection(clipRoot.clipId, true) + controller.requestAddToSelection(clipRoot.clipId, true) } clipMenu.clipId = clipRoot.clipId clipMenu.clipStatus = clipRoot.clipStatus clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor) clipMenu.grouped = clipRoot.grouped clipMenu.trackId = clipRoot.trackId clipMenu.canBeAudio = clipRoot.canBeAudio clipMenu.canBeVideo = clipRoot.canBeVideo clipMenu.popup() } } Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) Keys.onLeftPressed: { controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - 1, true, true, true); } Keys.onRightPressed: { controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + 1, true, true, true); } Keys.onUpPressed: { controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true); } Keys.onDownPressed: { controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true); } onPositionChanged: { var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x root.mousePosChanged(Math.round(mapped / timeline.scaleFactor)) if (mouse.modifiers & Qt.ShiftModifier) { timeline.position = Math.round(mapped / timeline.scaleFactor) } } onEntered: { var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height) initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false) } onExited: { endDrag() } onWheel: zoomByWheel(wheel) } Item { // Clipping container id: container anchors.fill: parent anchors.margins:1.5 clip: true Loader { id: thumbsLoader anchors.fill: parent source: parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : "" } Rectangle { // text background id: labelRect color: clipRoot.selected ? 'darkred' : '#66000000' width: label.width + 2 height: label.height visible: clipRoot.width > width / 2 Text { id: label text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '') font.pixelSize: root.baseUnit * 1.2 anchors { top: labelRect.top left: labelRect.left topMargin: 1 leftMargin: 1 } color: 'white' style: Text.Outline styleColor: 'black' } } Rectangle { // effects id: effectsRect color: '#555555' width: effectLabel.width + 2 height: effectLabel.height x: labelRect.x anchors.top: labelRect.bottom visible: labelRect.visible && clipRoot.effectNames != '' Text { id: effectLabel text: clipRoot.effectNames font.pixelSize: root.baseUnit * 1.2 anchors { top: effectsRect.top left: effectsRect.left topMargin: 1 leftMargin: 1 // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 } color: 'white' //style: Text.Outline styleColor: 'black' } } Repeater { model: markers delegate: Item { anchors.fill: parent Rectangle { id: markerBase width: 1 height: parent.height x: (model.frame - clipRoot.inPoint) * timeScale; color: model.color } Rectangle { visible: mlabel.visible opacity: 0.7 x: markerBase.x radius: 2 width: mlabel.width + 4 height: mlabel.height anchors { bottom: parent.verticalCenter } color: model.color MouseArea { z: 10 anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true onDoubleClicked: timeline.editMarker(clipRoot.binId, model.frame) onClicked: timeline.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers && parent.width > width * 1.5 text: model.comment font.pixelSize: root.baseUnit x: markerBase.x anchors { bottom: parent.verticalCenter topMargin: 2 leftMargin: 2 } color: 'white' } } } KeyframeView { id: effectRow visible: clipRoot.showKeyframes && clipRoot.keyframeModel selected: clipRoot.selected inPoint: clipRoot.inPoint outPoint: clipRoot.outPoint masterObject: clipRoot kfrModel: clipRoot.keyframeModel } } states: [ State { name: 'locked' when: isLocked PropertyChanges { target: clipRoot color: root.neutralColor opacity: 0.8 z: 0 } }, State { name: 'normal' when: clipRoot.selected === false PropertyChanges { target: clipRoot color: getColor() z: 0 } }, State { name: 'selected' when: clipRoot.selected === true PropertyChanges { target: clipRoot color: Qt.lighter(getColor(), 2) z: 3 } } ] TimelineTriangle { id: fadeInTriangle fillColor: 'green' width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width) height: clipRoot.height - clipRoot.border.width * 2 anchors.left: clipRoot.left anchors.top: clipRoot.top anchors.margins: clipRoot.border.width opacity: 0.3 } Rectangle { id: fadeInControl anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined anchors.top: fadeInTriangle.top anchors.topMargin: -10 width: root.baseUnit * 2 height: width radius: width / 2 color: '#FF66FFFF' border.width: 2 border.color: 'green' opacity: 0 Drag.active: fadeInMouseArea.drag.active MouseArea { id: fadeInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: parent drag.minimumX: -root.baseUnit drag.maximumX: container.width drag.axis: Drag.XAxis drag.smoothed: false property int startX property int startFadeIn onEntered: parent.opacity = 0.7 onExited: { if (!pressed) { parent.opacity = 0 } } onPressed: { root.stopScrolling = true startX = Math.round(parent.x / timeScale) startFadeIn = clipRoot.fadeIn parent.anchors.left = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 fadeInTriangle.opacity = 0.5 // parentTrack.clipSelected(clipRoot, parentTrack) TODO } onReleased: { root.stopScrolling = false fadeInTriangle.opacity = 0.3 parent.opacity = 0 if (fadeInTriangle.width > parent.radius) parent.anchors.horizontalCenter = fadeInTriangle.right else parent.anchors.left = fadeInTriangle.left console.log('released fade: ', clipRoot.fadeIn) timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn - 1, startFadeIn) bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round(parent.x / timeScale) - startX var duration = Math.max(0, startFadeIn + delta - 1) duration = Math.min(duration, clipRoot.clipDuration) if (duration != clipRoot.fadeIn - 1) { timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1) // Show fade duration as time in a "bubble" help. var s = timeline.timecode(Math.max(duration, 0)) bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s) } } } } SequentialAnimation on scale { loops: Animation.Infinite running: fadeInMouseArea.containsMouse && !fadeInMouseArea.pressed NumberAnimation { from: 1.0 to: 0.7 duration: 250 easing.type: Easing.InOutQuad } NumberAnimation { from: 0.7 to: 1.0 duration: 250 easing.type: Easing.InOutQuad } } } TimelineTriangle { id: fadeOutCanvas fillColor: 'red' width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width) height: clipRoot.height - clipRoot.border.width * 2 anchors.right: clipRoot.right anchors.top: clipRoot.top anchors.margins: clipRoot.border.width opacity: 0.3 transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2} } Rectangle { id: fadeOutControl anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined anchors.top: fadeOutCanvas.top anchors.topMargin: -10 width: root.baseUnit * 2 height: width radius: width / 2 color: '#66FFFFFF' border.width: 2 border.color: 'red' opacity: 0 Drag.active: fadeOutMouseArea.drag.active MouseArea { id: fadeOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: parent drag.axis: Drag.XAxis drag.minimumX: -root.baseUnit drag.maximumX: container.width property int startX property int startFadeOut onEntered: parent.opacity = 0.7 onExited: { if (!pressed) { parent.opacity = 0 } } drag.smoothed: false onPressed: { root.stopScrolling = true startX = Math.round(parent.x / timeScale) startFadeOut = clipRoot.fadeOut parent.anchors.right = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 fadeOutCanvas.opacity = 0.5 } onReleased: { fadeOutCanvas.opacity = 0.3 parent.opacity = 0 root.stopScrolling = false if (fadeOutCanvas.width > parent.radius) parent.anchors.horizontalCenter = fadeOutCanvas.left else parent.anchors.right = fadeOutCanvas.right timeline.adjustFade(clipRoot.clipId, 'fadeout', clipRoot.fadeOut, startFadeOut) bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = startX - Math.round(parent.x / timeScale) var duration = Math.max(0, startFadeOut + delta) duration = Math.min(duration, clipRoot.clipDuration) if (clipRoot.fadeOut != duration) { timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1) // Show fade duration as time in a "bubble" help. var s = timeline.timecode(Math.max(duration, 0)) bubbleHelp.show(clipRoot.x + clipRoot.width, parentTrack.y + clipRoot.height, s) } } } } SequentialAnimation on scale { loops: Animation.Infinite running: fadeOutMouseArea.containsMouse && !fadeOutMouseArea.pressed NumberAnimation { from: 1.0 to: 0.7 duration: 250 easing.type: Easing.InOutQuad } NumberAnimation { from: 0.7 to: 1.0 duration: 250 easing.type: Easing.InOutQuad } } } Rectangle { id: trimIn anchors.left: clipRoot.left anchors.leftMargin: 0 height: parent.height width: 5 color: isAudio? 'green' : 'lawngreen' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction visible: root.activeTool === 0 && !mouseArea.drag.active MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false property bool shiftTrim: false property bool sizeChanged: false cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); onPressed: { root.stopScrolling = true clipRoot.originalX = clipRoot.x clipRoot.originalDuration = clipDuration parent.anchors.left = undefined shiftTrim = mouse.modifiers & Qt.ShiftModifier parent.opacity = 0 } onReleased: { root.stopScrolling = false parent.anchors.left = clipRoot.left if (sizeChanged) { clipRoot.trimmedIn(clipRoot, shiftTrim) sizeChanged = false } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((trimIn.x) / timeScale) if (delta !== 0) { if (delta < -modelStart) { delta = -modelStart } var newDuration = clipDuration - delta sizeChanged = true clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim) } } } onEntered: { if (!pressed) { parent.opacity = 0.5 } } onExited: { parent.opacity = 0 } } } Rectangle { id: trimOut anchors.right: clipRoot.right anchors.rightMargin: 0 height: parent.height width: 5 color: 'red' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction visible: root.activeTool === 0 && !mouseArea.drag.active MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true property bool shiftTrim: false property bool sizeChanged: false cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false onPressed: { root.stopScrolling = true clipRoot.originalDuration = clipDuration parent.anchors.right = undefined shiftTrim = mouse.modifiers & Qt.ShiftModifier parent.opacity = 0 } onReleased: { root.stopScrolling = false parent.anchors.right = clipRoot.right if (sizeChanged) { clipRoot.trimmedOut(clipRoot, shiftTrim) sizeChanged = false } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var newDuration = Math.round((parent.x + parent.width) / timeScale) if (newDuration != clipDuration) { sizeChanged = true clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim) } } } onEntered: { if (!pressed) { parent.opacity = 0.5 } } onExited: parent.opacity = 0 } } /*MenuItem { id: mergeItem text: i18n('Merge with next clip') onTriggered: timeline.mergeClipWithNext(trackIndex, index, false) } MenuItem { text: i18n('Rebuild Audio Waveform') onTriggered: timeline.remakeAudioLevels(trackIndex, index) }*/ /*onPopupVisibleChanged: { if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) { // Try to fix menu running off screen. This only works intermittently. menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40)) menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width)) } }*/ } diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml index 83d84129f..820f9aa65 100644 --- a/src/timeline2/view/qml/timeline.qml +++ b/src/timeline2/view/qml/timeline.qml @@ -1,1381 +1,1374 @@ import QtQuick 2.6 import QtQml.Models 2.2 import QtQuick.Controls 1.4 as OLD import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQuick.Dialogs 1.2 import Kdenlive.Controls 1.0 import QtQuick.Window 2.2 import 'Timeline.js' as Logic Rectangle { id: root objectName: "timelineview" SystemPalette { id: activePalette } color: activePalette.window property bool validMenu: false property color textColor: activePalette.text signal clipClicked() signal mousePosChanged(int position) signal zoomIn(bool onMouse) signal zoomOut(bool onMouse) FontMetrics { id: fontMetrics font.family: "Arial" } ClipMenu { id: clipMenu } CompositionMenu { id: compositionMenu } function updatePalette() { root.color = activePalette.window root.textColor = activePalette.text playhead.fillColor = activePalette.windowText ruler.repaintRuler() } function moveSelectedTrack(offset) { var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack) var newTrack = cTrack + offset var max = tracksRepeater.count; if (newTrack < 0) { newTrack = max - 1; } else if (newTrack >= max) { newTrack = 0; } console.log('Setting curr tk: ', newTrack, 'MAX: ',max) timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId } function zoomByWheel(wheel) { if (wheel.modifiers & Qt.AltModifier) { if (wheel.angleDelta.x > 0) { timeline.triggerAction('monitor_seek_snap_backward') } else { timeline.triggerAction('monitor_seek_snap_forward') } } else if (wheel.modifiers & Qt.ControlModifier) { if (wheel.angleDelta.y > 0) { root.zoomIn(true); } else { root.zoomOut(true); } } else { var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width)) scrollView.flickableItem.contentX = Math.max(newScroll, 0) } wheel.accepted = true } function continuousScrolling(x) { // This provides continuous scrolling at the left/right edges. if (x > scrollView.flickableItem.contentX + scrollView.width - 50) { scrollTimer.item = clip scrollTimer.backwards = false scrollTimer.start() } else if (x < 50) { scrollView.flickableItem.contentX = 0; scrollTimer.stop() } else if (x < scrollView.flickableItem.contentX + 50) { scrollTimer.item = clip scrollTimer.backwards = true scrollTimer.start() } else { scrollTimer.stop() } } function getTrackYFromId(a_track) { return Logic.getTrackYFromId(a_track) } function getTrackYFromMltIndex(a_track) { return Logic.getTrackYFromMltIndex(a_track) } function getTracksCount() { return Logic.getTracksList() } function getMousePos() { return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor } function getScrollPos() { return scrollView.flickableItem.contentX } function setScrollPos(pos) { return scrollView.flickableItem.contentX = pos } function getCopiedItemId() { return copiedClip } function getMouseTrack() { return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY) } function getTrackColor(audio, header) { var col = activePalette.alternateBase if (audio) { col = Qt.tint(col, "#06FF00CC") } if (header) { col = Qt.darker(col, 1.05) } return col } function clearDropData() { clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false scrollTimer.stop() } function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) { dragProxy.x = itemObject.modelStart * timeScale dragProxy.y = itemCoord.y dragProxy.width = itemObject.clipDuration * timeScale dragProxy.height = itemCoord.height dragProxy.masterObject = itemObject dragProxy.draggedItem = itemId dragProxy.sourceTrack = itemTrack dragProxy.sourceFrame = itemPos dragProxy.isComposition = isComposition dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0 } function endDrag() { dragProxy.draggedItem = -1 dragProxy.x = 0 dragProxy.y = 0 dragProxy.width = 0 dragProxy.height = 0 dragProxy.verticalOffset = 0 } property int headerWidth: timeline.headerWidth() property int activeTool: 0 property real baseUnit: fontMetrics.font.pointSize property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2) property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3) property bool stopScrolling: false property int duration: timeline.duration property color audioColor: timeline.audioColor property color videoColor: timeline.videoColor property color neutralColor: timeline.neutralColor property color groupColor: timeline.groupColor property int clipBeingDroppedId: -1 property string clipBeingDroppedData property int droppedPosition: -1 property int droppedTrack: -1 property int clipBeingMovedId: -1 property int spacerGroup: -1 property int spacerFrame: -1 property int spacerClickFrame: -1 property real timeScale: timeline.scaleFactor property real snapping: timeline.snap ? 10 / Math.sqrt(timeScale) - 0.5 : -1 property var timelineSelection: timeline.selection property int trackHeight property int copiedClip: -1 property int zoomOnMouse: -1 property int viewActiveTrack: timeline.activeTrack //onCurrentTrackChanged: timeline.selection = [] onTimeScaleChanged: { if (root.zoomOnMouse >= 0) { scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX) root.zoomOnMouse = -1 } else { scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2)) } //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1 ruler.adjustStepSize() if (dragProxy.draggedItem > -1 && dragProxy.masterObject) { // update dragged item pos dragProxy.masterObject.updateDrag() } } onViewActiveTrackChanged: { var tk = Logic.getTrackById(timeline.activeTrack) if (tk.y < scrollView.flickableItem.contentY) { scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3) } else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) { scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3) } } DropArea { //Drop area for compositions width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/composition' onEntered: { console.log("Trying to drop composition") if (clipBeingMovedId == -1) { console.log("No clip being moved") var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame if (track >= 0 && !controller.isAudioTrack(track)) { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') console.log("Trying to insert",track, frame, clipBeingDroppedData) clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false) console.log("id",clipBeingDroppedId) continuousScrolling(drag.x + scrollView.flickableItem.contentX) drag.acceptProposedAction() } else { drag.accepted = false } } } onPositionChanged: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY) if (track !=-1) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping)) if (clipBeingDroppedId >= 0){ if (controller.isAudioTrack(track)) { // Don't allow moving composition to an audio track track = controller.getCompositionTrackId(clipBeingDroppedId) } controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else if (!controller.isAudioTrack(track)) { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } clearDropData() } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getCompositionPosition(clipBeingDroppedId) var track = controller.getCompositionTrackId(clipBeingDroppedId) // we simulate insertion at the final position so that stored undo has correct value controller.requestItemDeletion(clipBeingDroppedId, false) timeline.insertNewComposition(track, frame, clipBeingDroppedData, true) } clearDropData() } } DropArea { //Drop area for bin/clips /** @brief local helper function to handle the insertion of multiple dragged items */ function insertAndMaybeGroup(track, frame, droppedData) { var binIds = droppedData.split(";") if (binIds.length == 0) { return -1 } var id = -1 if (binIds.length == 1) { id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false) } else { var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false) // if the clip insertion succeeded, request the clips to be grouped if (ids.length > 0) { timeline.groupClips(ids) id = ids[0] } } return id } width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/producerslist' onEntered: { if (clipBeingMovedId == -1) { //var track = Logic.getTrackIdFromPos(drag.y) var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY) if (track >= 0 && track < tracksRepeater.count) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId //drag.acceptProposedAction() clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist') console.log('dropped data: ', clipBeingDroppedData) clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { drag.accepted = false } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } clearDropData() } onPositionChanged: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY) if (track >= 0 && track < tracksRepeater.count) { timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping)) if (clipBeingDroppedId >= 0){ controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getClipPosition(clipBeingDroppedId) var track = controller.getClipTrackId(clipBeingDroppedId) /* We simulate insertion at the final position so that stored undo has correct value * NOTE: even if dropping multiple clips, requesting the deletion of the first one is * enough as internally it will request the group deletion */ controller.requestItemDeletion(clipBeingDroppedId, false) var binIds = clipBeingDroppedData.split(";") if (binIds.length == 1) { timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false) } else { timeline.insertClips(track, frame, binIds, true, true) } } clearDropData() } } OLD.Menu { id: menu property int clickedX property int clickedY onAboutToHide: { timeline.ungrabHack() } OLD.MenuItem { text: i18n('Paste') iconName: 'edit-paste' visible: copiedClip != -1 onTriggered: { timeline.pasteItem() } } OLD.MenuItem { text: i18n('Insert Space') onTriggered: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY) var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) timeline.insertSpace(track, frame); } } OLD.MenuItem { text: i18n('Remove Space On Active Track') onTriggered: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY) var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) timeline.removeSpace(track, frame); } } OLD.MenuItem { text: i18n('Remove Space') onTriggered: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY) var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) timeline.removeSpace(track, frame, true); } } OLD.MenuItem { id: addGuideMenu text: i18n('Add Guide') onTriggered: { timeline.switchGuide(timeline.position); } } OLD.MenuItem { id: editGuideMenu text: i18n('Edit Guide') visible: false onTriggered: { timeline.editGuide(timeline.position); } } AssetMenu { title: i18n('Insert a composition...') menuModel: transitionModel isTransition: true onAssetSelected: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY) var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) var id = timeline.insertComposition(track, frame, assetId, true) if (id == -1) { compositionFail.open() } } } onAboutToShow: { if (guidesModel.hasMarker(timeline.position)) { // marker at timeline position addGuideMenu.text = i18n('Remove Guide') editGuideMenu.visible = true } else { addGuideMenu.text = i18n('Add Guide') editGuideMenu.visible = false } console.log("pop menu") } } OLD.Menu { id: rulermenu property int clickedX property int clickedY OLD.MenuItem { id: addGuideMenu2 text: i18n('Add Guide') onTriggered: { timeline.switchGuide(timeline.position); } } OLD.MenuItem { id: editGuideMenu2 text: i18n('Edit Guide') visible: false onTriggered: { timeline.editGuide(timeline.position); } } OLD.MenuItem { id: addProjectNote text: i18n('Add Project Note') onTriggered: { timeline.triggerAction('add_project_note') } } onAboutToShow: { if (guidesModel.hasMarker(timeline.position)) { // marker at timeline position addGuideMenu2.text = i18n('Remove Guide') editGuideMenu2.visible = true } else { addGuideMenu2.text = i18n('Add Guide') editGuideMenu2.visible = false } console.log("pop menu") } } MessageDialog { id: compositionFail title: i18n("Timeline error") icon: StandardIcon.Warning text: i18n("Impossible to add a composition at that position. There might not be enough space") standardButtons: StandardButton.Ok } OLD.Menu { id: headerMenu property int trackId: -1 property int thumbsFormat: 0 property bool audioTrack: false property bool recEnabled: false OLD.MenuItem { text: i18n('Add Track') onTriggered: { timeline.addTrack(timeline.activeTrack) timeline.ungrabHack() } } OLD.MenuItem { text: i18n('Delete Track') onTriggered: { timeline.deleteTrack(timeline.activeTrack) timeline.ungrabHack() } } OLD.MenuItem { visible: headerMenu.audioTrack id: showRec text: "Show Record Controls" onTriggered: { controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0') } checkable: true checked: headerMenu.recEnabled } OLD.Menu { title: i18n('Track thumbnails') visible: !headerMenu.audioTrack OLD.ExclusiveGroup { id: thumbStyle } OLD.MenuItem { text: "In frame" id: inFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2) checkable: true exclusiveGroup: thumbStyle } OLD.MenuItem { text: "In / out frames" id: inOutFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0) checkable: true checked: true exclusiveGroup: thumbStyle } OLD.MenuItem { text: "All frames" id: allFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1) checkable: true exclusiveGroup: thumbStyle } OLD.MenuItem { text: "No thumbnails" id: noFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3) checkable: true exclusiveGroup: thumbStyle } onAboutToShow: { switch(headerMenu.thumbsFormat) { case 3: noFrame.checked = true break case 2: inFrame.checked = true break case 1: allFrame.checked = true break default: inOutFrame.checked = true break } } } } Row { Column { id: headerContainer z: 1 Rectangle { id: cornerstone property bool selected: false // Padding between toolbar and track headers. width: headerWidth height: ruler.height color: 'transparent' //selected? shotcutBlue : activePalette.window border.color: selected? 'red' : 'transparent' border.width: selected? 1 : 0 z: 1 - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - //timeline.selectMultitrack() - } - } } Flickable { // Non-slider scroll area for the track headers. id: headerFlick contentY: scrollView.flickableItem.contentY width: headerWidth height: 100 interactive: false MouseArea { width: trackHeaders.width height: trackHeaders.height acceptedButtons: Qt.NoButton onWheel: { var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height) scrollView.flickableItem.contentY = Math.max(newScroll, 0) } } Column { id: trackHeaders spacing: 0 Repeater { id: trackHeaderRepeater model: multitrack TrackHead { trackName: model.name thumbsFormat: model.thumbsFormat trackTag: model.trackTag isDisabled: model.disabled isComposite: model.composite isLocked: model.locked isAudio: model.audio showAudioRecord: model.audioRecord effectNames: model.effectNames isStackEnabled: model.isStackEnabled width: headerWidth height: model.trackHeight selected: false current: item === timeline.activeTrack trackId: item onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked collapsed: height <= collapsedHeight onMyTrackHeightChanged: { trackBaseRepeater.itemAt(index).height = myTrackHeight tracksRepeater.itemAt(index).height = myTrackHeight height = myTrackHeight collapsed = height <= collapsedHeight if (!collapsed) { controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight) controller.setTrackProperty(trackId, "kdenlive:collapsed", "0") } else { controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight) } // hack: change property to trigger transition adjustment root.trackHeight = root.trackHeight === 1 ? 0 : 1 } onClicked: { timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId) //timeline.selectTrackHead(currentTrack) } } } } Column { id: trackHeadersResizer spacing: 0 width: 5 Rectangle { id: resizer height: trackHeaders.height width: 3 x: root.headerWidth - 2 color: 'red' opacity: 0 Drag.active: headerMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: headerMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.minimumX: 2 * baseUnit property double startX property double originalX drag.smoothed: false onPressed: { root.stopScrolling = true } onReleased: { root.stopScrolling = false parent.opacity = 0 } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { parent.opacity = 0.5 headerWidth = Math.max(10, mapToItem(null, x, y).x + 2) timeline.setHeaderWidth(headerWidth) } } } } } } } MouseArea { id: tracksArea property real clickX property real clickY width: root.width - headerWidth height: root.height Keys.onDownPressed: { root.moveSelectedTrack(1) } Keys.onUpPressed: { root.moveSelectedTrack(-1) } // This provides continuous scrubbing and scimming at the left/right edges. hoverEnabled: true acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor onWheel: { if (wheel.modifiers & Qt.AltModifier) { // Alt + wheel = seek to next snap point if (wheel.angleDelta.x > 0) { timeline.triggerAction('monitor_seek_snap_backward') } else { timeline.triggerAction('monitor_seek_snap_forward') } } else { var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1 if (timeline.seekPosition > -1) { timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1) } else { timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1) } timeline.position = timeline.seekPosition } } onPressed: { focus = true if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier)) { clickX = mouseX clickY = mouseY return } if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) { // rubber selection rubberSelect.x = mouse.x + tracksArea.x rubberSelect.y = mouse.y rubberSelect.originX = mouse.x rubberSelect.originY = rubberSelect.y rubberSelect.width = 0 rubberSelect.height = 0 } else if (mouse.button & Qt.LeftButton) { if (dragProxy.draggedItem > -1) { mouse.accepted = false return } if (root.activeTool === 2 && mouse.y > ruler.height) { // spacer tool var y = mouse.y - ruler.height + scrollView.flickableItem.contentY var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1 spacerGroup = timeline.requestSpacerStartOperation(track, frame) if (spacerGroup > -1) { drag.axis = Drag.XAxis Drag.active = true Drag.proposedAction = Qt.MoveAction spacerClickFrame = frame spacerFrame = controller.getClipPosition(spacerGroup) } } else if (root.activeTool === 0 || mouse.y <= ruler.height) { if (mouse.y > ruler.height) { - timeline.selection = [] + controller.requestClearSelection(); } timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1) timeline.position = timeline.seekPosition } else if (root.activeTool === 1) { // razor tool var y = mouse.y - ruler.height + scrollView.flickableItem.contentY timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId) } } else if (mouse.button & Qt.RightButton) { menu.clickedX = mouse.x menu.clickedY = mouse.y if (mouse.y > ruler.height) { timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId menu.popup() } else { // ruler menu rulermenu.popup() } } } property bool scim: false onExited: { scim = false } onPositionChanged: { if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier))) { var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width)) var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height) scrollView.flickableItem.contentX = Math.max(newScroll, 0) scrollView.flickableItem.contentY = Math.max(vertScroll, 0) clickX = mouseX clickY = mouseY return } if (dragProxy.draggedItem > -1) { mouse.accepted = false return } var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)) root.mousePosChanged(mousePos) ruler.showZoneLabels = mouse.y < ruler.height if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) { // rubber selection rubberSelect.visible = true } if (rubberSelect.visible) { var newX = mouse.x var newY = mouse.y if (newX < rubberSelect.originX) { rubberSelect.x = newX + tracksArea.x rubberSelect.width = rubberSelect.originX - newX } else { rubberSelect.x = rubberSelect.originX + tracksArea.x rubberSelect.width = newX - rubberSelect.originX } if (newY < rubberSelect.originY) { rubberSelect.y = newY rubberSelect.height = rubberSelect.originY - newY } else { rubberSelect.y = rubberSelect.originY rubberSelect.height= newY - rubberSelect.originY } } else if (mouse.buttons === Qt.LeftButton) { if (root.activeTool === 0 || mouse.y < ruler.height) { timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)) timeline.position = timeline.seekPosition } else if (root.activeTool === 2 && spacerGroup > -1) { // Move group var track = controller.getItemTrackId(spacerGroup) var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping)) continuousScrolling(mouse.x + scrollView.flickableItem.contentX) } scim = true } else { scim = false if (root.activeTool === 1) { cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX if (mouse.modifiers & Qt.ShiftModifier) { timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) } } } } onReleased: { if (rubberSelect.visible) { rubberSelect.visible = false var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y)) var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height) if (bottomTrack >= topTrack) { var t = [] for (var i = topTrack; i <= bottomTrack; i++) { t.push(tracksRepeater.itemAt(i).trackInternalId) } var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier); } rubberSelect.y = -1 } else if (mouse.modifiers & Qt.ShiftModifier) { // Shift click, process seek timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1) timeline.position = timeline.seekPosition } if (spacerGroup > -1) { var frame = controller.getItemPosition(spacerGroup) timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame); spacerClickFrame = -1 spacerFrame = -1 spacerGroup = -1 } scim = false } Timer { id: scrubTimer interval: 25 repeat: true running: parent.scim && parent.containsMouse && (parent.mouseX < 50 || parent.mouseX > parent.width - 50) && (timeline.position * timeline.scaleFactor >= 50) onTriggered: { if (parent.mouseX < 50) timeline.seekPosition = Math.max(0, timeline.position - 10) else timeline.seekPosition = Math.min(timeline.position + 10, timeline.fullDuration - 1) } } Column { Flickable { // Non-slider scroll area for the Ruler. id: rulercontainer width: root.width - headerWidth height: fontMetrics.font.pixelSize * 2 contentX: scrollView.flickableItem.contentX contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale) interactive: false clip: true Ruler { id: ruler width: rulercontainer.contentWidth height: parent.height Rectangle { id: seekCursor visible: timeline.seekPosition > -1 color: activePalette.highlight width: 4 height: ruler.height opacity: 0.5 x: timeline.seekPosition * timeline.scaleFactor } TimelinePlayhead { id: playhead visible: timeline.position > -1 height: baseUnit width: baseUnit * 1.5 fillColor: activePalette.windowText anchors.bottom: parent.bottom x: timeline.position * timeline.scaleFactor - (width / 2) } } } OLD.ScrollView { id: scrollView width: root.width - headerWidth height: root.height - ruler.height y: ruler.height // Click and drag should seek, not scroll the timeline view flickableItem.interactive: false clip: true Rectangle { id: tracksContainerArea width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale) height: trackHeaders.height //Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height) color: root.color Rectangle { // Drag proxy, responsible for clip / composition move id: dragProxy x: 0 y: 0 width: 0 height: 0 property int draggedItem: -1 property int sourceTrack property int sourceFrame property bool isComposition property int verticalOffset property var masterObject color: 'green' opacity: 0.8 MouseArea { id: dragProxyArea anchors.fill: parent drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false drag.minimumX: 0 property int dragFrame property bool shiftClick: false cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++') if (mouse.modifiers & Qt.ControlModifier) { mouse.accepted = false return } dragFrame = -1 timeline.activeTrack = dragProxy.sourceTrack if (mouse.modifiers & Qt.ShiftModifier) { if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) { console.log('ADD SELECTION: ', dragProxy.draggedItem) - timeline.addSelection(dragProxy.draggedItem) + controller.requestAddToSelection(dragProxy.draggedItem) } else { console.log('REMOVE SELECTION: ', dragProxy.draggedItem) - timeline.removeSelection(dragProxy.draggedItem) + controller.requestRemoveFromSelection(dragProxy.draggedItem) //endDrag() shiftClick = true return } shiftClick = true } else { if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) { - timeline.selection = [ dragProxy.draggedItem ] + controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true) } shiftClick = false } timeline.showAsset(dragProxy.draggedItem) root.stopScrolling = true clipBeingMovedId = dragProxy.draggedItem if (dragProxy.draggedItem > -1) { var tk = controller.getItemTrackId(dragProxy.draggedItem) var x = controller.getItemPosition(dragProxy.draggedItem) var posx = Math.round((parent.x)/ root.timeScale) var clickAccepted = true if (controller.normalEdit() && (tk != Logic.getTrackIdFromPos(parent.y) || x != posx)) { console.log('INCORRECT DRAG, Trying to recover item: ', parent.y,' XPOS: ',x,'=',posx,'\n!!!!!!!!!!') // Try to find correct item var track = Logic.getTrackById(tk) var container = track.children[0].children[0].children[0] var tentativeClip = container.childAt(mouseX + parent.x, 5) if (tentativeClip && tentativeClip.clipId) { clickAccepted = true dragProxy.draggedItem = tentativeClip.clipId dragProxy.x = tentativeClip.x dragProxy.y = tentativeClip.y dragProxy.width = tentativeClip.width dragProxy.height = tentativeClip.height dragProxy.masterObject = tentativeClip dragProxy.sourceTrack = tk dragProxy.sourceFrame = tentativeClip.modelStart dragProxy.isComposition = tentativeClip.isComposition } else { clickAccepted = false mouse.accepted = false dragProxy.draggedItem = -1 dragProxy.masterObject = undefined parent.x = 0 parent.y = 0 parent.width = 0 parent.height = 0 } } if (clickAccepted && dragProxy.draggedItem != -1) { focus = true; dragProxy.masterObject.originalX = dragProxy.masterObject.x dragProxy.masterObject.originalTrackId = dragProxy.masterObject.trackId dragProxy.masterObject.forceActiveFocus(); } } else { mouse.accepted = false parent.x = 0 parent.y = 0 parent.width = 0 parent.height = 0 } } onPositionChanged: { if (!shiftClick && dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton) { continuousScrolling(mouse.x + parent.x) var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x root.mousePosChanged(Math.round(mapped / timeline.scaleFactor)) var posx = Math.round((parent.x)/ root.timeScale) var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height) var tId = Logic.getTrackIdFromPos(posy) if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) { if (posx == dragFrame) { return } } if (dragProxy.isComposition) { dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping)) timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem) } else { if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) { var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y); dragProxy.masterObject.parent = dragContainer pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y) dragProxy.masterObject.x = pos.x dragProxy.masterObject.y = pos.y console.log('bringing item to front') } dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping)) timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem) } var delta = dragFrame - dragProxy.sourceFrame if (delta != 0) { var s = timeline.timecode(Math.abs(delta)) // remove leading zeroes if (s.substring(0, 3) === '00:') s = s.substring(3) s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s bubbleHelp.show(parent.x, ruler.height, s) } else bubbleHelp.hide() } } onReleased: { clipBeingMovedId = -1 if (!shiftClick && dragProxy.draggedItem > -1 && dragFrame > -1) { var tId = controller.getItemTrackId(dragProxy.draggedItem) if (dragProxy.isComposition) { controller.requestCompositionMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false) controller.requestCompositionMove(dragProxy.draggedItem, tId, dragFrame , true, true, true) } else { if (controller.normalEdit()) { controller.requestClipMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false) controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , true, true, true) } else { // Fake move, only process final move timeline.endFakeMove(dragProxy.draggedItem, dragFrame , true, true, true) } } dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor dragProxy.sourceFrame = dragFrame bubbleHelp.hide() } } onDoubleClicked: { if (dragProxy.masterObject.keyframeModel) { var newVal = (dragProxy.height - mouseY) / dragProxy.height var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal) } } } } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: zoomByWheel(wheel) cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape } Column { // These make the striped background for the tracks. // It is important that these are not part of the track visual hierarchy; // otherwise, the clips will be obscured by the Track's background. Repeater { model: multitrack id: trackBaseRepeater delegate: Rectangle { width: tracksContainerArea.width border.width: 1 border.color: root.frameColor height: model.trackHeight color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red' } } } Column { id: tracksContainer Repeater { id: tracksRepeater; model: trackDelegateModel } Item { id: dragContainer } Repeater { id: guidesRepeater; model: guidesDelegateModel } } Rectangle { id: cursor visible: timeline.position > -1 color: root.textColor width: Math.max(1, 1 * timeline.scaleFactor) opacity: (width > 2) ? 0.5 : 1 height: parent.height x: timeline.position * timeline.scaleFactor } } } } /*CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0 } CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0 anchors.right: parent.right mirrorGradient: true }*/ Rectangle { id: cutLine visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height color: 'red' width: Math.max(1, 1 * timeline.scaleFactor) opacity: (width > 2) ? 0.5 : 1 height: root.height - scrollView.__horizontalScrollBar.height - ruler.height x: 0 //x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX y: ruler.height } } } Rectangle { id: bubbleHelp property alias text: bubbleHelpLabel.text color: root.color //application.toolTipBaseColor width: bubbleHelpLabel.width + 8 height: bubbleHelpLabel.height + 8 radius: 4 states: [ State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} }, State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} } ] state: 'invisible' transitions: [ Transition { from: 'invisible' to: 'visible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } }, Transition { from: 'visible' to: 'invisible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } } ] Label { id: bubbleHelpLabel color: activePalette.text //application.toolTipTextColor anchors.centerIn: parent font.pixelSize: root.baseUnit } function show(x, y, text) { bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height bubbleHelp.text = text if (bubbleHelp.state !== 'visible') bubbleHelp.state = 'visible' } function hide() { bubbleHelp.state = 'invisible' bubbleHelp.opacity = 0 } } Rectangle { id: rubberSelect property int originX property int originY y: -1 color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4) border.color: activePalette.highlight border.width: 1 visible: false } /*DropShadow { source: bubbleHelp anchors.fill: bubbleHelp opacity: bubbleHelp.opacity horizontalOffset: 3 verticalOffset: 3 radius: 8 color: '#80000000' transparentBorder: true fast: true }*/ DelegateModel { id: trackDelegateModel model: multitrack delegate: Track { trackModel: multitrack rootIndex: trackDelegateModel.modelIndex(index) height: trackHeight timeScale: timeline.scaleFactor width: tracksContainerArea.width isAudio: audio trackThumbsFormat: thumbsFormat isCurrentTrack: item === timeline.activeTrack trackInternalId: item z: tracksRepeater.count - index } } DelegateModel { id: guidesDelegateModel model: guidesModel Item { id: guideRoot z: 20 Rectangle { id: guideBase width: 1 height: tracksContainer.height x: model.frame * timeScale; color: model.color } Rectangle { visible: mlabel.visible opacity: 0.7 x: guideBase.x y: mlabel.y radius: 2 width: mlabel.width + 4 height: mlabel.height color: model.color MouseArea { z: 10 anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true property int startX drag.axis: Drag.XAxis drag.target: guideRoot onPressed: { drag.target = guideRoot startX = guideRoot.x } onReleased: { if (startX != guideRoot.x) { timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor) } drag.target = undefined } onPositionChanged: { if (pressed) { var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, root.snapping) guideRoot.x = (frame - model.frame) * timeline.scaleFactor } } drag.smoothed: false onDoubleClicked: { timeline.editGuide(model.frame) drag.target = undefined } onClicked: timeline.position = guideBase.x / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers text: model.comment font.pixelSize: root.baseUnit x: guideBase.x + 2 y: scrollView.flickableItem.contentY color: 'white' } } } Connections { target: timeline onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded() onFrameFormatChanged: ruler.adjustFormat() onSelectionChanged: { //cornerstone.selected = timeline.isMultitrackSelected() if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) { endDrag() } var selectedTrack = timeline.selectedTrack() for (var i = 0; i < trackHeaderRepeater.count; i++) { trackHeaderRepeater.itemAt(i).selected = (i === selectedTrack) } } } // This provides continuous scrolling at the left/right edges. Timer { id: scrollTimer interval: 25 repeat: true triggeredOnStart: true property var item property bool backwards onTriggered: { var delta = backwards? -10 : 10 if (item) item.x += delta scrollView.flickableItem.contentX += delta if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1) stop() } } } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index a4f3c4a24..d063e76f8 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,2553 +1,2056 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "dialogs/speeddialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); m_disablePreview->setEnabled(false); } TimelineController::~TimelineController() { delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); - m_selection.selectedItems.clear(); - m_selection.selectedTrack = -1; connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); - connect(m_model.get(), &TimelineModel::removeFromSelection, this, &TimelineController::slotUpdateSelection); + connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); } void TimelineController::setTargetTracks(QPair targets) { setVideoTarget(targets.first >= 0 && targets.first < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.first) : -1); setAudioTarget(targets.second >= 0 && targets.second < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.second) : -1); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } -void TimelineController::removeSelection(int newSelection) -{ - if (!m_selection.selectedItems.contains(newSelection)) { - return; - } - m_selection.selectedItems.removeAll(newSelection); - std::unordered_set ids; - ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); - if (ids.size() > 1) { - m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); - } else if (m_model->m_temporarySelectionGroup > -1) { - m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); - m_model->m_temporarySelectionGroup = -1; - } - - std::unordered_set newIds; - if (m_model->m_temporarySelectionGroup >= 0) { - // new items were selected, inform model to prepare for group drag - newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); - } - emit selectionChanged(); - if (!m_selection.selectedItems.isEmpty()) - emitSelectedFromSelection(); - else - emit selected(nullptr); -} - -void TimelineController::addSelection(int newSelection, bool clear) -{ - if (m_selection.selectedItems.contains(newSelection)) { - return; - } - if (clear) { - if (m_model->m_temporarySelectionGroup >= 0) { - m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); - m_model->m_temporarySelectionGroup = -1; - } - m_selection.selectedItems.clear(); - } - m_selection.selectedItems << newSelection; - std::unordered_set ids; - ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); - m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); - - std::unordered_set newIds; - if (m_model->m_temporarySelectionGroup >= 0) { - // new items were selected, inform model to prepare for group drag - newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); - } - emit selectionChanged(); - if (!m_selection.selectedItems.isEmpty()) - emitSelectedFromSelection(); - else - emit selected(nullptr); -} - int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor - if (m_selection.selectedItems.isEmpty()) { + auto selection = m_model->getCurrentSelection(); + if (selection.empty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection - return m_selection.selectedItems.constFirst(); + return *(selection.begin()); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } -std::unordered_set TimelineController::getCurrentSelectionIds() const +int TimelineController::selectedTrack() const { - std::unordered_set selection; - if (m_model->m_temporarySelectionGroup >= 0 || - (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { - selection = m_model->getGroupElements(m_selection.selectedItems.constFirst()); - } else { - for (int i : m_selection.selectedItems) { - selection.insert(i); - } + std::unordered_set sel = m_model->getCurrentSelection(); + if (sel.empty()) return -1; + std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item + for (int s : sel) { + int tid = m_model->getItemTrackId(s); + selected_tracks.push_back({m_model->getTrackPosition(tid), tid}); } - return selection; + // sort by track position + std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); + return selected_tracks.front().second; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, timelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } - if (addToCurrent || !select) { - toSelect = m_selection.selectedItems; - } - if (select) { - if (!toSelect.contains(currentClip)) { - toSelect << currentClip; - setSelection(toSelect); - } - } else if (toSelect.contains(currentClip)) { - toSelect.removeAll(currentClip); - setSelection(toSelect); - } -} - -void TimelineController::setSelection(const QList &newSelection, int trackIndex, bool isMultitrack) -{ - qDebug() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack; - if (newSelection != selection() || trackIndex != m_selection.selectedTrack || isMultitrack != m_selection.isMultitrackSelected) { - m_selection.selectedItems = newSelection; - m_selection.selectedTrack = trackIndex; - m_selection.isMultitrackSelected = isMultitrack; - if (m_model->m_temporarySelectionGroup > -1) { - // Clear current selection - m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); - } - std::unordered_set newIds; - if (m_selection.selectedItems.size() > 0) { - std::unordered_set ids; - ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); - m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); - if (m_model->m_temporarySelectionGroup >= 0 || - (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { - newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); - } else { - qDebug() << "// NON GROUPED SELCTUIIN: " << m_selection.selectedItems << " !!!!!!"; - } - emitSelectedFromSelection(); - } else { - // Empty selection - emit selected(nullptr); - emit showItemEffectStack(QString(), nullptr, QSize(), false); - } - emit selectionChanged(); - } -} - -void TimelineController::emitSelectedFromSelection() -{ - /*if (!m_model.trackList().count()) { - if (m_model.tractor()) - selectMultitrack(); - else - emit selected(0); - return; + if (!select) { + m_model->requestRemoveFromSelection(currentClip); + } else { + m_model->requestAddToSelection(currentClip, !addToCurrent); } - - int trackIndex = currentTrack(); - int clipIndex = selection().isEmpty()? 0 : selection().first(); - Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex); - if (info && info->producer && info->producer->is_valid()) { - delete m_updateCommand; - m_updateCommand = new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start); - // We need to set these special properties so time-based filters - // can get information about the cut while still applying filters - // to the cut parent. - info->producer->set(kFilterInProperty, info->frame_in); - info->producer->set(kFilterOutProperty, info->frame_out); - if (MLT.isImageProducer(info->producer)) - info->producer->set("out", info->cut->get_int("out")); - info->producer->set(kMultitrackItemProperty, 1); - m_ignoreNextPositionChange = true; - emit selected(info->producer); - delete info; - }*/ } QList TimelineController::selection() const { if (!m_root) return QList(); - return m_selection.selectedItems; + QList items; + for (int id : m_model->getCurrentSelection()) { + items << id; + } + return items; } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } -void TimelineController::selectMultitrack() -{ - setSelection(QList(), -1, true); - QMetaObject::invokeMethod(m_root, "selectMultitrack"); - // emit selected(m_model.tractor()); -} - void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } void TimelineController::snapChanged(bool snap) { m_root->setProperty("snapping", snap ? 10 / std::sqrt(m_scale) : -1); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimum = m_model->getClipPosition(clipId); int clip_duration = m_model->getClipPlaytime(clipId); int position = minimum; if (offset > clip_duration / 2) { position += offset; } int duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = false; if (lowerVideoTrackId > 0) { int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position); if (bottomId > 0) { QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimum && position > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { position = bottom.first; duration = test_duration; revert = true; } } } int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (duration2 > 0) { duration = (duration > 0) ? qMin(duration, duration2) : duration2; } } if (duration <= 4) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); } std::unique_ptr props(nullptr); if (revert) { props = std::make_unique(); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { - if (m_selection.selectedItems.isEmpty()) { + auto sel = m_model->getCurrentSelection(); + if (sel.empty()) { return; } - if (m_model->m_temporarySelectionGroup != -1) { - // selection is grouped, delete group only - m_selection.selectedItems.clear(); - emit selectionChanged(); - m_model->requestGroupDeletion(m_model->m_temporarySelectionGroup); - return; - } else { - for (int cid : m_selection.selectedItems) { - m_model->requestItemDeletion(cid); - } - } - m_selection.selectedItems.clear(); - emit selectionChanged(); -} - -void TimelineController::slotUpdateSelection(int itemId) -{ - if (m_selection.selectedItems.contains(itemId)) { - m_selection.selectedItems.removeAll(itemId); - emit selectionChanged(); - } + // only need to delete the first item, the others will be deleted in cascade + m_model->requestItemDeletion(*sel.begin()); } void TimelineController::copyItem() { - int clipId = -1; - if (!m_selection.selectedItems.isEmpty()) { - clipId = m_selection.selectedItems.first(); - // Check grouped clips - QList extraClips = m_selection.selectedItems; - // TODO better guess for master track - int masterTid = m_model->getItemTrackId(m_selection.selectedItems.first()); - bool audioCopy = m_model->isAudioTrack(masterTid); - int masterTrack = m_model->getTrackPosition(masterTid); - std::unordered_set groupRoots; - for (int id : m_selection.selectedItems) { - if (m_model->m_groups->isInGroup(id)) { - int gid = m_model->m_groups->getRootId(id); - qDebug() << " * ** ITEM " << id << " IS IN GROUP: " << gid; - if (gid != m_model->m_temporarySelectionGroup) { - qDebug() << " * ** TRYING TO INSERT GP: " << gid; - if (groupRoots.find(gid) == groupRoots.end()) { - groupRoots.insert(gid); - } - } else { - qDebug() << " * ** TRYING TO INSERT SELECTION CHILD"; - std::unordered_set selection = m_model->m_groups->getDirectChildren(gid); - for (int j : selection) { - if (groupRoots.find(j) == groupRoots.end()) { - groupRoots.insert(j); - } - } - } - std::unordered_set selection = m_model->getGroupElements(id); - for (int j : selection) { - if (!extraClips.contains(j)) { - extraClips << j; - } - } - } - } - qDebug() << "==============\n GROUP ROOTS: "; - for (int gp : groupRoots) { - qDebug() << "GROUP: " << gp; - } - qDebug() << "\n======="; - QDomDocument copiedItems; - int offset = -1; - QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); - copiedItems.appendChild(container); - QStringList binIds; - for (int id : extraClips) { - if (offset == -1 || m_model->getItemPosition(id) < offset) { - offset = m_model->getItemPosition(id); - } - if (m_model->isClip(id)) { - container.appendChild(m_model->m_allClips[id]->toXml(copiedItems)); - const QString bid = m_model->m_allClips[id]->binId(); - if (!binIds.contains(bid)) { - binIds << bid; - } - } else if (m_model->isComposition(id)) { - container.appendChild(m_model->m_allCompositions[id]->toXml(copiedItems)); - } - } - QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); - container.appendChild(container2); - for (const QString &id : binIds) { - std::shared_ptr clip = pCore->bin()->getBinClip(id); - QDomDocument tmp; - container2.appendChild(clip->toXml(tmp)); - } - container.setAttribute(QStringLiteral("offset"), offset); - if (audioCopy) { - int masterMirror = m_model->getMirrorVideoTrackId(masterTid); - if (masterMirror == -1) { - QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(m_model); - if (!projectTracks.second.isEmpty()) { - masterTrack = m_model->getTrackPosition(projectTracks.second.first()); - } - } else { - masterTrack = m_model->getTrackPosition(masterMirror); - } - } - /* masterTrack contains the reference track over which we want to paste. - this is a video track, unless audioCopy is defined */ - container.setAttribute(QStringLiteral("masterTrack"), masterTrack); - container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); - QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); - container.appendChild(grp); - grp.appendChild(copiedItems.createTextNode(m_model->m_groups->toJson(groupRoots))); - // TODO: groups - qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; - QClipboard *clipboard = QApplication::clipboard(); - clipboard->setText(copiedItems.toString()); - } else { + std::unordered_set selectedIds = m_model->getCurrentSelection(); + if (selectedIds.empty()) { return; } + int clipId = *(selectedIds.begin()); + QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(copyString); m_root->setProperty("copiedClip", clipId); } bool TimelineController::pasteItem() { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); - QDomDocument copiedItems; - copiedItems.setContent(txt); - if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { - qDebug() << " / / READING CLIPS FROM CLIPBOARD"; - } else { - return false; - } int tid = getMouseTrack(); int position = getMousePos(); if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } - std::function undo = []() { return true; }; - std::function redo = []() { return true; }; - const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); - QMap mappedIds; - // Check available tracks - QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(m_model); - int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt(); - QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); - QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); - // find paste tracks - // List of all source audio tracks - QList audioTracks; - // List of all source video tracks - QList videoTracks; - // List of all audio tracks with their corresponding video mirror - QMap audioMirrors; - // List of all source audio tracks that don't have video mirror - QList singleAudioTracks; - for (int i = 0; i < clips.count(); i++) { - QDomElement prod = clips.at(i).toElement(); - int trackPos = prod.attribute(QStringLiteral("track")).toInt(); - bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack")); - if (audioTrack) { - if (!audioTracks.contains(trackPos)) { - audioTracks << trackPos; - } - int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt(); - if (videoMirror == -1) { - if (singleAudioTracks.contains(trackPos)) { - continue; - } - singleAudioTracks << trackPos; - continue; - } - audioMirrors.insert(trackPos, videoMirror); - if (videoTracks.contains(videoMirror)) { - continue; - } - videoTracks << videoMirror; - } else { - if (videoTracks.contains(trackPos)) { - continue; - } - videoTracks << trackPos; - } - } - for (int i = 0; i < compositions.count(); i++) { - QDomElement prod = compositions.at(i).toElement(); - int trackPos = prod.attribute(QStringLiteral("track")).toInt(); - if (!videoTracks.contains(trackPos)) { - videoTracks << trackPos; - } - int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt(); - if (atrackPos == 0 || videoTracks.contains(atrackPos)) { - continue; - } - videoTracks << atrackPos; - } - // Now we have a list of all source tracks, check that we have enough target tracks - qSort(videoTracks); - qSort(audioTracks); - qSort(singleAudioTracks); - int requestedVideoTracks = videoTracks.isEmpty() ? 0 : videoTracks.last() - videoTracks.first() + 1; - int requestedAudioTracks = audioTracks.isEmpty() ? 0 : audioTracks.last() - audioTracks.first() + 1; - if (requestedVideoTracks > projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) { - pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); - return false; - } - - // Check we have enough tracks above/below - if (requestedVideoTracks > 0) { - qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks; - int tracksBelow = masterSourceTrack - videoTracks.first(); - int tracksAbove = videoTracks.last() - masterSourceTrack; - qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove; - qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(tid) << ", IX: " << tid; - qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(tid); - if (projectTracks.second.indexOf(tid) < tracksBelow) { - qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; - // not enough tracks below, try to paste on upper track - tid = projectTracks.second.at(tracksBelow); - } else if ((projectTracks.second.size() - (projectTracks.second.indexOf(tid) + 1)) < tracksAbove) { - // not enough tracks above, try to paste on lower track - qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove); - tid = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1); - } - } - QMap tracksMap; - int masterIx = projectTracks.second.indexOf(tid); - qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << tid; - qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack; - qDebug() << "/// MASTER PASTE: " << masterIx; - for (int tk : videoTracks) { - tracksMap.insert(tk, projectTracks.second.at(masterIx + tk - masterSourceTrack)); - qDebug() << "// TK MAP: " << tk << " => " << projectTracks.second.at(masterIx + tk - masterSourceTrack); - } - QMapIterator it(audioMirrors); - while (it.hasNext()) { - it.next(); - int videoIx = tracksMap.value(it.value()); - // qDebug()<<"// TK AUDIO MAP: "< "<getMirrorAudioTrackId(videoIx); - tracksMap.insert(it.key(), m_model->getMirrorAudioTrackId(videoIx)); - } - for (int i = 0; i < singleAudioTracks.size(); i++) { - tracksMap.insert(singleAudioTracks.at(i), projectTracks.first.at(i)); - } - qDebug() << "++++++++++++++++++++++++++\n\n\n// AUDIO MIRRORS: " << audioMirrors << ", RESULT: " << tracksMap; - if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { - // paste from another document, import bin clips - QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); - if (folderId.isEmpty()) { - // Folder doe not exist - const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); - folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); - pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); - } - QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); - for (int i = 0; i < binClips.count(); ++i) { - QDomElement currentProd = binClips.item(i).toElement(); - QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); - if (!pCore->projectItemModel()->isIdFree(clipId)) { - QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); - Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); - mappedIds.insert(clipId, updatedId); - clipId = updatedId; - } - pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo); - } - } - - int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); - - bool res = true; - QLocale locale; - QMap correspondingIds; - QList waitingIds; - for (int i = 0; i < clips.count(); i++) { - waitingIds << i; - } - for (int i = 0; res && !waitingIds.isEmpty();) { - if (i >= waitingIds.size()) { - i = 0; - } - QDomElement prod = clips.at(waitingIds.at(i)).toElement(); - QString originalId = prod.attribute(QStringLiteral("binid")); - if (mappedIds.contains(originalId)) { - // Map id - originalId = mappedIds.value(originalId); - } - int in = prod.attribute(QStringLiteral("in")).toInt(); - int out = prod.attribute(QStringLiteral("out")).toInt(); - int trackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); - int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; - double speed = locale.toDouble(prod.attribute(QStringLiteral("speed"))); - int newId; - bool created = m_model->requestClipCreation(originalId, newId, m_model->getTrackById_const(trackId)->trackType(), speed, undo, redo); - if (created) { - // Master producer is ready - // ids.removeAll(originalId); - waitingIds.removeAt(i); - } else { - i++; - qApp->processEvents(); - continue; - } - if (m_model->m_allClips[newId]->m_endlessResize) { - out = out - in; - in = 0; - m_model->m_allClips[newId]->m_producer->set("length", out + 1); - } - m_model->m_allClips[newId]->setInOut(in, out); - correspondingIds.insert(prod.attribute(QStringLiteral("id")).toInt(), newId); - res = res & m_model->getTrackById(trackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo); - // paste effects - if (res) { - std::shared_ptr destStack = m_model->getClipEffectStackModel(newId); - destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo); - } - } - - // Compositions - for (int i = 0; res && i < compositions.count(); i++) { - QDomElement prod = compositions.at(i).toElement(); - QString originalId = prod.attribute(QStringLiteral("composition")); - int in = prod.attribute(QStringLiteral("in")).toInt(); - int out = prod.attribute(QStringLiteral("out")).toInt(); - int trackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); - int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt(); - if (aTrackId > 0) { - aTrackId = m_model->getTrackPosition(tracksMap.value(aTrackId)); - } - int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; - int newId; - auto transProps = std::make_unique(); - QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); - for (int j = 0; j < props.count(); j++) { - transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), - props.at(j).toElement().text().toUtf8().constData()); - } - res = m_model->requestCompositionInsertion(originalId, trackId, aTrackId, position + pos, out - in, std::move(transProps), newId, undo, redo); - } - if (!res) { - undo(); - return false; - } - const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); - // Rebuild groups - m_model->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo); - pCore->pushUndo(undo, redo, i18n("Paste clips")); - return true; + return TimelineFunctions::pasteClips(m_model, txt, tid, position); } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int newTid; m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); m_model->buildTrackCompositing(true); m_model->_resetView(); } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); m_model->buildTrackCompositing(true); if (m_activeTrack == selectedTrackIx) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::gotoNextSnap() { setPosition(m_model->requestNextSnapPos(timelinePosition())); } void TimelineController::gotoPreviousSnap() { setPosition(m_model->requestPreviousSnapPos(timelinePosition())); } void TimelineController::groupSelection() { - if (m_selection.selectedItems.size() < 2) { + const auto selection = m_model->getCurrentSelection(); + if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } - std::unordered_set clips; - for (int id : m_selection.selectedItems) { - clips.insert(id); - } - m_model->requestClipsGroup(clips); - emit selectionChanged(); + m_model->requestClearSelection(); + m_model->requestClipsGroup(selection); + m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { - if (cid == -1 && m_selection.selectedItems.isEmpty()) { - pCore->displayMessage(i18n("Select at least 1 item to ungroup"), InformationMessage, 500); - return; - } + auto ids = m_model->getCurrentSelection(); // ask to unselect if needed - int tmpGroup = m_model->m_temporarySelectionGroup; - if (tmpGroup >= 0) { - m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); - } - std::unordered_set ids; - if (cid > -1 && cid != tmpGroup) { + m_model->requestClearSelection(); + if (cid > -1) { ids.insert(cid); } - for (int id : m_selection.selectedItems) { - ids.insert(id); - } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } - m_selection.selectedItems.clear(); - emit selectionChanged(); } void TimelineController::setInPoint() { int cursorPos = timelinePosition(); - if (!m_selection.selectedItems.isEmpty()) { - for (int id : m_selection.selectedItems) { + const auto selection = m_model->getCurrentSelection(); + if (!selection.empty()) { + for (int id : selection) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { int cursorPos = timelinePosition(); - if (!m_selection.selectedItems.isEmpty()) { - for (int id : m_selection.selectedItems) { + const auto selection = m_model->getCurrentSelection(); + if (!selection.empty()) { + for (int id : selection) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); } } } void TimelineController::editMarker(const QString &cid, int frame) { std::shared_ptr clip = pCore->bin()->getBinClip(cid); GenTime pos(frame, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = timelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = timelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); - if (!m_selection.selectedItems.isEmpty()) { + const auto selection = m_model->getCurrentSelection(); + if (!selection.empty()) { QList effectSelection; - for (int id : m_selection.selectedItems) { + for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; int partner = m_model->getClipSplitPartner(id); if (partner > -1 && !effectSelection.contains(partner)) { effectSelection << partner; } } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setSeekPosition(int position) { m_seekPosition = position; emit seekPositionChanged(); } void TimelineController::onSeeked(int position) { m_position = position; emit positionChanged(); if (m_seekPosition > -1 && position == m_seekPosition) { m_seekPosition = -1; emit seekPositionChanged(); } } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } -void TimelineController::selectItems(const QVariantList &arg, int startFrame, int endFrame, bool addToSelect) +void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect) { - std::unordered_set previousSelection = getCurrentSelectionIds(); std::unordered_set itemsToSelect; if (addToSelect) { - for (int cid : m_selection.selectedItems) { - itemsToSelect.insert(cid); - } + itemsToSelect = m_model->getCurrentSelection(); } - m_selection.selectedItems.clear(); - for (int i = 0; i < arg.count(); i++) { - auto currentClips = m_model->getItemsInRange(arg.at(i).toInt(), startFrame, endFrame, true); + for (int i = 0; i < tracks.count(); i++) { + auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } - if (itemsToSelect.size() > 0) { - for (int x : itemsToSelect) { - m_selection.selectedItems << x; - } - qDebug() << "// GROUPING ITEMS: " << m_selection.selectedItems; - m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(itemsToSelect, true, GroupType::Selection); - qDebug() << "// GROUPING ITEMS DONE"; - } else if (m_model->m_temporarySelectionGroup > -1) { - m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); - } - std::unordered_set newIds; - if (m_model->m_temporarySelectionGroup >= 0) { - newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); - for (int child : newIds) { - QModelIndex ix; - if (m_model->isClip(child)) { - ix = m_model->makeClipIndexFromID(child); - } else if (m_model->isComposition(child)) { - ix = m_model->makeCompositionIndexFromID(child); - } - if (ix.isValid()) { - m_model->dataChanged(ix, ix, {TimelineModel::GroupedRole}); - } - } - } - emit selectionChanged(); + m_model->requestSetSelection(itemsToSelect); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = timelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; - for (int cid : m_selection.selectedItems) { + const auto selection = m_model->getCurrentSelection(); + for (int cid : selection) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); } void TimelineController::seekCurrentClip(bool seekToEnd) { - for (int cid : m_selection.selectedItems) { + const auto selection = m_model->getCurrentSelection(); + for (int cid : selection) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) { return; } if (m_position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, timelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(Mlt::Filter *filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } int clipId = getCurrentItem(); if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); initializePreview(); } } void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(timelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } // find blank duration int spaceDuration = m_model->getTrackById_const(trackId)->getBlankSizeAtPos(frame); int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start - spaceDuration); } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || m_model->getItemTrackId(cid) == -1) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, double speed) { if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); - bool ok = false; double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)) - 1); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)) - 1); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } // speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok); QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0)); if (d->exec() != QDialog::Accepted) { return; } speed = d->getValue(); qDebug() << "requesting speed " << speed; } m_model->requestClipTimeWarp(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 1; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, QVector() << track, zone, false); } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; if (clip->hasAudio()) { aTrack = audioTarget(); } if (clip->hasVideo()) { vTrack = videoTarget(); } if (aTrack == -1 && vTrack == -1) { // No target tracks defined, use active track if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { aTrack = m_activeTrack; vTrack = m_model->getMirrorVideoTrackId(aTrack); } else { vTrack = m_activeTrack; aTrack = m_model->getMirrorAudioTrackId(vTrack); } } int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x()) : -1; } void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(int clipId) { TimelineFunctions::switchEnableState(m_model, clipId); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { int track = m_model->getClipTrackId(clipId); insertNewComposition(track, clipId, offset, assetId, true); } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { - int cid = m_selection.selectedItems.first(); + int cid = *m_model->getCurrentSelection().begin(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift + m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); if (m_model->m_groups->isInGroup(clipId)) { std::unordered_set groupIds = m_model->getGroupElements(clipId); // Check that no item is grouped with our audioRef item // TODO - clearSelection(); + m_model->requestClearSelection(); } const QString otherBinId = getClipBinId(clipId); if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId); if (newPos) { bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(getClipBinId(clipId), clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId), (size_t)m_model->getClipPosition(clipId)); m_audioCorrelator->addChild(envelope); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:locked_track").toInt() == 1; m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1")); } else { // Invert track lock // Get track states first QMap trackLockState; int unlockedTracksCount = 0; int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1; if (!isLocked) { unlockedTracksCount++; } trackLockState.insert(trackIx, isLocked); } if (unlockedTracksCount == tracksCount) { // do not lock all tracks, leave active track unlocked trackLockState.insert(m_activeTrack, true); } QMapIterator i(trackLockState); while (i.hasNext()) { i.next(); m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1")); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } int TimelineController::groupClips(const QList &clipIds) { std::unordered_set theSet(clipIds.begin(), clipIds.end()); return m_model->requestClipsGroup(theSet, false, GroupType::Selection); } bool TimelineController::ungroupClips(int clipId) { return m_model->requestClipUngroup(clipId); } -void TimelineController::clearSelection() -{ - if (m_model->m_temporarySelectionGroup >= 0) { - m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); - m_model->m_temporarySelectionGroup = -1; - } - m_selection.selectedItems.clear(); - emit selectionChanged(); -} - void TimelineController::selectAll() { - QList ids; - for (const auto &clp : m_model->m_allClips) { - ids << clp.first; + std::unordered_set ids; + for (auto clp : m_model->m_allClips) { + ids.insert(clp.first); } - for (const auto &clp : m_model->m_allCompositions) { - ids << clp.first; + for (auto clp : m_model->m_allCompositions) { + ids.insert(clp.first); } - setSelection(ids); + m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { - QList ids; - for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { - ids << clp.first; + std::unordered_set ids; + for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { + ids.insert(clp.first); } - for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { - ids << clp.first; + for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { + ids.insert(clp.first); } - setSelection(ids); + m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getCopiedItemId", Q_RETURN_ARG(QVariant, returnedValue)); int sourceId = returnedValue.toInt(); - if (targetId == -1 && !m_selection.selectedItems.isEmpty()) { - targetId = m_selection.selectedItems.constFirst(); + if (targetId == -1 && !m_model->getCurrentSelection().empty()) { + targetId = *m_model->getCurrentSelection().begin(); } if (!m_model->isClip(targetId) || !m_model->isClip(sourceId)) { return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr sourceStack = m_model->getClipEffectStackModel(sourceId); std::shared_ptr destStack = m_model->getClipEffectStackModel(targetId); bool result = destStack->importEffects(sourceStack, m_model->m_allClips[targetId]->clipState(), undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { - if (m_selection.selectedItems.isEmpty()) { + if (m_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); return; } std::shared_ptr clip(nullptr); - int item = m_selection.selectedItems.first(); + int item = *m_model->getCurrentSelection().begin(); if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { - enableAction = m_model->isInMultiSelection(item); + enableAction = isInSelection(item); } else if (actionData == QLatin1Char('U')) { - enableAction = m_model->isInMultiSelection(item) || (m_model->m_groups->isInGroup(item) && !m_model->isInMultiSelection(item)); + enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item)); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { - if (m_selection.selectedItems.isEmpty()) { + if (m_model->getCurrentSelection().empty()) { // TODO: error displayMessage return; } - int id = m_selection.selectedItems.constFirst(); + int id = *m_model->getCurrentSelection().begin(); if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeClipIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeCompositionIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = m_model->m_allClips[itemId]->getFakeTrackId(); return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = res & m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res & TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res & TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res & TimelineFunctions::insertSpace(m_model, trackId, QPoint(position, position + duration), undo, redo); } res = res & m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res & TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QList processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res & TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res & TimelineFunctions::insertSpace(m_model, -1, QPoint(min, max), undo, redo); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, updateView, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { - return m_model->isInMultiSelection(itemId); + return m_model->getCurrentSelection().count(itemId) > 0; } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable) { TimelineFunctions::enableMultitrackView(m_model, enable); } void TimelineController::saveTimelineSelection(const QDir &targetDir) { - TimelineFunctions::saveTimelineSelection(m_model, m_selection.selectedItems, targetDir); + TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::LinkBackground).color().darker(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::NegativeBackground).color(); } QColor TimelineController::neutralColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::VisitedBackground).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.background(KColorScheme::NegativeBackground).color(); } diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index b454d312a..33cccd0fc 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,530 +1,514 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINECONTROLLER_H #define TIMELINECONTROLLER_H #include "definitions.h" #include "lib/audio/audioCorrelation.h" #include "timeline2/model/timelineitemmodel.hpp" #include #include class PreviewManager; class QAction; class QQuickItem; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TimelineController : public QObject { Q_OBJECT /* @brief holds a list of currently selected clips (list of clipId's) */ - Q_PROPERTY(QList selection READ selection WRITE setSelection NOTIFY selectionChanged) + Q_PROPERTY(QList selection READ selection NOTIFY selectionChanged) /* @brief holds the timeline zoom factor */ Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged) /* @brief holds the current project duration */ Q_PROPERTY(int duration READ duration NOTIFY durationChanged) Q_PROPERTY(int fullDuration READ fullDuration NOTIFY durationChanged) Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged) /* @brief holds the current timeline position */ Q_PROPERTY(int position READ position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int seekPosition READ seekPosition WRITE setSeekPosition NOTIFY seekPositionChanged) Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged) Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged) Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged) Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged) Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged) Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged) Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged) Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged) Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged) Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged) Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged) Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged) Q_PROPERTY(QColor videoColor READ videoColor NOTIFY colorsChanged) Q_PROPERTY(QColor audioColor READ audioColor NOTIFY colorsChanged) Q_PROPERTY(QColor neutralColor READ neutralColor NOTIFY colorsChanged) Q_PROPERTY(QColor groupColor READ groupColor NOTIFY colorsChanged) public: TimelineController(QObject *parent); ~TimelineController() override; /** @brief Sets the model that this widgets displays */ void setModel(std::shared_ptr model); std::shared_ptr getModel() const; void setRoot(QQuickItem *root); - - Q_INVOKABLE bool isMultitrackSelected() const { return m_selection.isMultitrackSelected; } - Q_INVOKABLE int selectedTrack() const { return m_selection.selectedTrack; } - /** @brief Remove a clip id from current selection - */ - Q_INVOKABLE void removeSelection(int newSelection); - /** @brief Add a clip id to current selection - */ - Q_INVOKABLE void addSelection(int newSelection, bool clear = false); /** @brief Edit an item's in/out points with a dialog */ Q_INVOKABLE void editItemDuration(int itemId); - /** @brief Clear current selection and inform the view - */ - void clearSelection(); + + /** @brief Returns the topmost track containing a selected item (-1 if selection is embty) */ + Q_INVOKABLE int selectedTrack() const; + + /** @brief Select the clip in active track under cursor + @param type is the type of the object (clip or composition) + @param select: true if the object should be selected and false if it should be deselected + @param addToCurrent: if true, the object will be added to the new selection + */ + void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false); + /** @brief Select all timeline items */ void selectAll(); /* @brief Select all items in one track */ void selectCurrentTrack(); + /** @brief Select multiple objects on the timeline + @param tracks List of ids of tracks from which to select + @param start/endFrame Interval from which to select the items + @param addToSelect if true, the old selection is retained + */ + Q_INVOKABLE void selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect); + /* @brief Returns true is item is selected as well as other items */ + Q_INVOKABLE bool isInSelection(int itemId); + /* @brief returns current timeline's zoom factor */ Q_INVOKABLE double scaleFactor() const; /* @brief set current timeline's zoom factor */ void setScaleFactorOnMouse(double scale, bool zoomOnMouse); void setScaleFactor(double scale); /* @brief Returns the project's duration (tractor) */ Q_INVOKABLE int duration() const; Q_INVOKABLE int fullDuration() const; /* @brief Returns the current cursor position (frame currently displayed by MLT) */ Q_INVOKABLE int position() const { return m_position; } /* @brief Returns the seek request position (-1 = no seek pending) */ Q_INVOKABLE int seekPosition() const { return m_seekPosition; } Q_INVOKABLE int audioTarget() const; Q_INVOKABLE int videoTarget() const; Q_INVOKABLE int activeTrack() const { return m_activeTrack; } Q_INVOKABLE QColor videoColor() const; Q_INVOKABLE QColor audioColor() const; Q_INVOKABLE QColor neutralColor() const; Q_INVOKABLE QColor groupColor() const; /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE int zoneIn() const { return m_zone.x(); } Q_INVOKABLE int zoneOut() const { return m_zone.y(); } Q_INVOKABLE void setZoneIn(int inPoint); Q_INVOKABLE void setZoneOut(int outPoint); void setZone(const QPoint &zone); /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE void setPosition(int position); Q_INVOKABLE bool snap(); Q_INVOKABLE bool ripple(); Q_INVOKABLE bool scrub(); Q_INVOKABLE QString timecode(int frames); /* @brief Request inserting a new clip in timeline (dragged from bin or monitor) @param tid is the destination track @param position is the timeline position @param xml is the data describing the dropped clip @param logUndo if set to false, no undo object is stored @return the id of the inserted clip */ Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView, bool useTargets); /* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor) * @param tid is the destination track * @param position is the timeline position * @param binIds the IDs of the bins being dropped * @param logUndo if set to false, no undo object is stored * @return the ids of the inserted clips */ Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView); /* @brief Request the grouping of the given clips * @param clipIds the ids to be grouped * @return the group id or -1 in case of faiure */ Q_INVOKABLE int groupClips(const QList &clipIds); /* @brief Request the ungrouping of clips * @param clipId the id of a clip belonging to the group * @return true in case of success, false otherwise */ Q_INVOKABLE bool ungroupClips(int clipId); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(); /* @brief Request inserting a new composition in timeline (dragged from compositions list) @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo); /* @brief Request inserting a new composition in timeline (dragged from compositions list) this function will check if there is a clip at insert point and adjust the composition length accordingly @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo); Q_INVOKABLE int insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo); /* @brief Request deletion of the currently selected clips */ Q_INVOKABLE void deleteSelectedClips(); Q_INVOKABLE void triggerAction(const QString &name); /* @brief Do we want to display video thumbnails */ bool showThumbnails() const; bool showAudioThumbnails() const; bool showMarkers() const; bool audioThumbFormat() const; /* @brief Do we want to display audio thumbnails */ Q_INVOKABLE bool showWaveforms() const; /* @brief Insert a timeline track */ Q_INVOKABLE void addTrack(int tid); /* @brief Remove a timeline track */ Q_INVOKABLE void deleteTrack(int tid); /* @brief Group selected items in timeline */ Q_INVOKABLE void groupSelection(); /* @brief Ungroup selected items in timeline */ Q_INVOKABLE void unGroupSelection(int cid = -1); /* @brief Ask for edit marker dialog */ Q_INVOKABLE void editMarker(const QString &cid, int frame); /* @brief Ask for edit timeline guide dialog */ Q_INVOKABLE void editGuide(int frame = -1); Q_INVOKABLE void moveGuide(int frame, int newFrame); /* @brief Add a timeline guide */ Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false); /* @brief Request monitor refresh */ Q_INVOKABLE void requestRefresh(); /* @brief Show the asset of the given item in the AssetPanel If the id corresponds to a clip, we show the corresponding effect stack If the id corresponds to a composition, we show its properties */ Q_INVOKABLE void showAsset(int id); Q_INVOKABLE void showTrackAsset(int trackId); - Q_INVOKABLE void selectItems(const QVariantList &arg, int startFrame, int endFrame, bool addToSelect); - /* @brief Returns true is item is selected as well as other items */ - Q_INVOKABLE bool isInSelection(int itemId); Q_INVOKABLE bool exists(int itemId); Q_INVOKABLE int headerWidth() const; Q_INVOKABLE void setHeaderWidth(int width); /* @brief Seek to next snap point */ void gotoNextSnap(); /* @brief Seek to previous snap point */ void gotoPreviousSnap(); /* @brief Set current item's start point to cursor position */ void setInPoint(); /* @brief Set current item's end point to cursor position */ void setOutPoint(); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); - /* @brief Sets the list of currently selected clips - @param selection is the list of id's - @param trackIndex is current clip's track - @param isMultitrack is true if we want to select the whole tractor (currently unused) - */ - void setSelection(const QList &selection = QList(), int trackIndex = -1, bool isMultitrack = false); /* @brief Get the list of currently selected clip id's */ QList selection() const; /* @brief Add an asset (effect, composition) */ void addAsset(const QVariantMap &data); /* @brief Cuts the clip on current track at timeline position */ Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1); /* @brief Request a spacer operation */ Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position); /* @brief Request a spacer operation */ Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition); /* @brief Request a Fade in effect for clip */ Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration); Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos); /* @brief Request inserting space in a track */ Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1); Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false); /* @brief If clip is enabled, disable, otherwise enable */ Q_INVOKABLE void switchEnableState(int clipId); Q_INVOKABLE void addCompositionToClip(const QString &assetId, int clipId, int offset); Q_INVOKABLE void addEffectToClip(const QString &assetId, int clipId); Q_INVOKABLE void requestClipCut(int clipId, int position); Q_INVOKABLE void extract(int clipId); Q_INVOKABLE void splitAudio(int clipId); Q_INVOKABLE void splitVideo(int clipId); Q_INVOKABLE void setAudioRef(int clipId); Q_INVOKABLE void alignAudio(int clipId); Q_INVOKABLE bool endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline); Q_INVOKABLE int getItemMovingTrack(int itemId) const; bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo); bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo); bool splitAV(); /* @brief Seeks to selected clip start / end */ Q_INVOKABLE void pasteEffects(int targetId = -1); Q_INVOKABLE double fps() const; Q_INVOKABLE void addEffectKeyframe(int cid, int frame, double val); Q_INVOKABLE void removeEffectKeyframe(int cid, int frame); Q_INVOKABLE void updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue = QVariant()); void switchTrackLock(bool applyToAll = false); void switchTargetTrack(); const QString getTrackNameFromIndex(int trackIndex); /* @brief Seeks to selected clip start / end */ void seekCurrentClip(bool seekToEnd = false); /* @brief Seeks to a clip start (or end) based on it's clip id */ void seekToClip(int cid, bool seekToEnd); /* @brief Returns the number of tracks (audioTrakcs, videoTracks) */ QPoint getTracksCount() const; /* @brief Request monitor refresh if item (clip or composition) is under timeline cursor */ void refreshItem(int id); /* @brief Seek timeline to mouse position */ void seekToMouse(); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(bool snap); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Get the frame where mouse is positioned */ int getMousePos(); /* @brief Get the frame where mouse is positioned */ int getMouseTrack(); /* @brief Returns a map of track ids/track names */ QMap getTrackNames(bool videoOnly); /* @brief Returns the transition a track index for a composition (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; const QString getClipBinId(int clipId) const; void focusItem(int itemId); /* @brief Create and display a split clip view to compare effect */ bool createSplitOverlay(Mlt::Filter *filter); /* @brief Delete the split clip view to compare effect */ void removeSplitOverlay(); /* @brief Add current timeline zone to preview rendering */ void addPreviewRange(bool add); /* @brief Clear current timeline zone from preview rendering */ void clearPreviewRange(); void startPreviewRender(); void stopPreviewRender(); QVariantList dirtyChunks() const; QVariantList renderedChunks() const; /* @brief returns the frame currently processed by timeline preview, -1 if none */ int workingPreview() const; /** @brief Return true if we want to use timeline ruler zone for editing */ bool useRuler() const; /* @brief Load timeline preview from saved doc */ void loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable); /* @brief Return document properties with added settings from timeline */ QMap documentProperties(); /** @brief Change track compsiting mode */ void switchCompositing(int mode); /** @brief Change a clip item's speed in timeline */ Q_INVOKABLE void changeItemSpeed(int clipId, double speed); /** @brief Delete selected zone and fill gap by moving following clips * @param lift if true, the zone will simply be deleted but clips won't be moved */ void extractZone(QPoint zone, bool liftOnly = false); /** @brief Insert clip monitor into timeline * @returns the zone end position or -1 on fail */ int insertZone(const QString &binId, QPoint zone, bool overwrite); void updateClip(int clipId, const QVector &roles); void showClipKeyframes(int clipId, bool value); void showCompositionKeyframes(int clipId, bool value); /** @brief Returns last usable timeline position (seek request or current pos) */ int timelinePosition() const; /** @brief Adjust all timeline tracks height */ void resetTrackHeight(); /** @brief timeline preview params changed, reset */ void resetPreview(); - /** @brief Select the clip in active track under cursor */ - void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false); /** @brief Set target tracks (video, audio) */ void setTargetTracks(QPair targets); /** @brief Return asset's display name from it's id (effect or composition) */ Q_INVOKABLE const QString getAssetName(const QString &assetId, bool isTransition); /** @brief Set keyboard grabbing on current selection */ void grabCurrent(); /** @brief Returns keys for all used thumbnails */ QStringList getThumbKeys(); public slots: - void selectMultitrack(); void resetView(); Q_INVOKABLE void setSeekPosition(int position); Q_INVOKABLE void setAudioTarget(int track); Q_INVOKABLE void setVideoTarget(int track); Q_INVOKABLE void setActiveTrack(int track); void onSeeked(int position); void addEffectToCurrentClip(const QStringList &effectData); /** @brief Dis / enable timeline preview. */ void disablePreview(bool disable); void invalidateItem(int cid); void invalidateZone(int in, int out); void checkDuration(); /** @brief Dis / enable multi track view. */ void slotMultitrackView(bool enable); /** @brief Save timeline selected clips to target folder. */ void saveTimelineSelection(const QDir &targetDir); /** @brief Restore timeline scroll pos on open. */ void setScrollPos(int pos); private slots: - void slotUpdateSelection(int itemId); void updateClipActions(); public: /** @brief a list of actions that have to be enabled/disabled depending on the timeline selection */ QList clipActions; private: QQuickItem *m_root; KActionCollection *m_actionCollection; std::shared_ptr m_model; bool m_usePreview; - struct Selection - { - QList selectedItems; - int selectedTrack; - bool isMultitrackSelected; - }; int m_position; int m_seekPosition; int m_audioTarget; int m_videoTarget; int m_activeTrack; int m_audioRef; QPoint m_zone; double m_scale; static int m_duration; - Selection m_selection; - Selection m_savedSelection; PreviewManager *m_timelinePreview; QAction *m_disablePreview; std::shared_ptr m_audioCorrelator; QMutex m_metaMutex; - void emitSelectedFromSelection(); int getCurrentItem(); void initializePreview(); - // Get a list of currently selected items, including clips grouped with selection - std::unordered_set getCurrentSelectionIds() const; signals: void selected(Mlt::Producer *producer); void selectionChanged(); void frameFormatChanged(); void trackHeightChanged(); void scaleFactorChanged(); void audioThumbFormatChanged(); void durationChanged(); void positionChanged(); void seekPositionChanged(); void audioTargetChanged(); void videoTargetChanged(); void activeTrackChanged(); void colorsChanged(); void showThumbnailsChanged(); void showAudioThumbnailsChanged(); void showMarkersChanged(); void rippleChanged(); void scrubChanged(); void seeked(int position); void zoneChanged(); void zoneMoved(const QPoint &zone); /* @brief Requests that a given parameter model is displayed in the asset panel */ void showTransitionModel(int tid, std::shared_ptr); void showItemEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /* @brief notify of chunks change */ void dirtyChunksChanged(); void renderedChunksChanged(); void workingPreviewChanged(); void useRulerChanged(); void updateZoom(double); /* @brief emitted when timeline selection changes, true if a clip is selected */ void timelineClipSelected(bool); Q_INVOKABLE void ungrabHack(); }; #endif diff --git a/src/timeline2/view/timelinewidget.cpp b/src/timeline2/view/timelinewidget.cpp index b1d53e756..88e7e839a 100644 --- a/src/timeline2/view/timelinewidget.cpp +++ b/src/timeline2/view/timelinewidget.cpp @@ -1,200 +1,205 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinewidget.h" #include "../model/builders/meltBuilder.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "capture/mediacapture.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/model/effectfilter.hpp" #include "effects/effectlist/model/effecttreemodel.hpp" #include "kdenlivesettings.h" #include "mainwindow.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qml/timelineitems.h" #include "qmltypes/thumbnailprovider.h" #include "timelinecontroller.h" #include "transitions/transitionlist/model/transitionfilter.hpp" #include "transitions/transitionlist/model/transitiontreemodel.hpp" #include "utils/clipboardproxy.hpp" #include #include // #include #include #include #include #include #include const int TimelineWidget::comboScale[] = {1, 2, 4, 8, 15, 30, 50, 75, 100, 150, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 6000, 15000, 30000}; TimelineWidget::TimelineWidget(QWidget *parent) : QQuickWidget(parent) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); #endif setClearColor(palette().window().color()); registerTimelineItems(); // Build transition model for context menu m_transitionModel = TransitionTreeModel::construct(true, this); m_transitionProxyModel = std::make_unique(this); static_cast(m_transitionProxyModel.get())->setFilterType(true, TransitionType::Favorites); m_transitionProxyModel->setSourceModel(m_transitionModel.get()); m_transitionProxyModel->setSortRole(AssetTreeModel::NameRole); m_transitionProxyModel->sort(0, Qt::AscendingOrder); // Build effects model for context menu m_effectsModel = EffectTreeModel::construct(QStringLiteral(), this); m_effectsProxyModel = std::make_unique(this); static_cast(m_effectsProxyModel.get())->setFilterType(true, EffectType::Favorites); m_effectsProxyModel->setSourceModel(m_effectsModel.get()); m_effectsProxyModel->setSortRole(AssetTreeModel::NameRole); m_effectsProxyModel->sort(0, Qt::AscendingOrder); m_proxy = new TimelineController(this); connect(m_proxy, &TimelineController::zoneMoved, this, &TimelineWidget::zoneMoved); connect(m_proxy, &TimelineController::ungrabHack, this, &TimelineWidget::slotUngrabHack); setResizeMode(QQuickWidget::SizeRootObjectToView); m_thumbnailer = new ThumbnailProvider; engine()->addImageProvider(QStringLiteral("thumbnail"), m_thumbnailer); setVisible(false); setFocusPolicy(Qt::StrongFocus); // connect(&*m_model, SIGNAL(seeked(int)), this, SLOT(onSeeked(int))); } TimelineWidget::~TimelineWidget() { delete m_proxy; } void TimelineWidget::updateEffectFavorites() { rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); } void TimelineWidget::updateTransitionFavorites() { rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); } const QStringList TimelineWidget::sortedItems(const QStringList &items, bool isTransition) { QMap sortedItems; for (const QString &effect : items) { sortedItems.insert(m_proxy->getAssetName(effect, isTransition), effect); } return sortedItems.values(); } void TimelineWidget::setModel(const std::shared_ptr &model) { m_thumbnailer->resetProject(); m_sortModel = std::make_unique(this); m_sortModel->setSourceModel(model.get()); m_sortModel->setSortRole(TimelineItemModel::SortRole); m_sortModel->sort(0, Qt::DescendingOrder); m_proxy->setModel(model); rootContext()->setContextProperty("multitrack", m_sortModel.get()); rootContext()->setContextProperty("controller", model.get()); rootContext()->setContextProperty("timeline", m_proxy); rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); // m_transitionProxyModel.get()); // rootContext()->setContextProperty("effectModel", m_effectsProxyModel.get()); rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); rootContext()->setContextProperty("audiorec", pCore->getAudioDevice()); rootContext()->setContextProperty("guidesModel", pCore->projectManager()->current()->getGuideModel().get()); rootContext()->setContextProperty("clipboard", new ClipboardProxy(this)); setSource(QUrl(QStringLiteral("qrc:/qml/timeline.qml"))); connect(rootObject(), SIGNAL(mousePosChanged(int)), pCore->window(), SLOT(slotUpdateMousePosition(int))); connect(rootObject(), SIGNAL(zoomIn(bool)), pCore->window(), SLOT(slotZoomIn(bool))); connect(rootObject(), SIGNAL(zoomOut(bool)), pCore->window(), SLOT(slotZoomOut(bool))); m_proxy->setRoot(rootObject()); setVisible(true); loading = false; m_proxy->checkDuration(); m_proxy->positionChanged(); } void TimelineWidget::mousePressEvent(QMouseEvent *event) { emit focusProjectMonitor(); QQuickWidget::mousePressEvent(event); } void TimelineWidget::slotChangeZoom(int value, bool zoomOnMouse) { double pixelScale = QFontMetrics(font()).maxWidth() * 2; m_proxy->setScaleFactorOnMouse(pixelScale / comboScale[value], zoomOnMouse); } Mlt::Tractor *TimelineWidget::tractor() { return m_proxy->tractor(); } TimelineController *TimelineWidget::controller() { return m_proxy; } +std::shared_ptr TimelineWidget::model() +{ + return m_proxy->getModel(); +} + void TimelineWidget::zoneUpdated(const QPoint &zone) { m_proxy->setZone(zone); } void TimelineWidget::setTool(ProjectTool tool) { rootObject()->setProperty("activeTool", (int)tool); } QPoint TimelineWidget::getTracksCount() const { return m_proxy->getTracksCount(); } void TimelineWidget::slotUngrabHack() { // Workaround bug: https://bugreports.qt.io/browse/QTBUG-59044 // https://phabricator.kde.org/D5515 if (quickWindow() && quickWindow()->mouseGrabberItem()) { quickWindow()->mouseGrabberItem()->ungrabMouse(); } } int TimelineWidget::zoomForScale(double value) const { int scale = 100.0 / value; int ix = 13; while (comboScale[ix] > scale && ix > 0) { ix--; } return ix; } diff --git a/src/timeline2/view/timelinewidget.h b/src/timeline2/view/timelinewidget.h index 9cce09fdf..9eab0eaf7 100644 --- a/src/timeline2/view/timelinewidget.h +++ b/src/timeline2/view/timelinewidget.h @@ -1,87 +1,88 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINEWIDGET_H #define TIMELINEWIDGET_H #include "assets/assetlist/model/assetfilter.hpp" #include "assets/assetlist/model/assettreemodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include class ThumbnailProvider; class KActionCollection; class AssetParameterModel; class TimelineController; class QSortFilterProxyModel; class TimelineWidget : public QQuickWidget { Q_OBJECT public: TimelineWidget(QWidget *parent = Q_NULLPTR); ~TimelineWidget() override; /* @brief Sets the model shown by this widget */ void setModel(const std::shared_ptr &model); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); TimelineController *controller(); + std::shared_ptr model(); void setTool(ProjectTool tool); QPoint getTracksCount() const; /* @brief calculate zoom level for a scale */ int zoomForScale(double value) const; bool loading; protected: void mousePressEvent(QMouseEvent *event) override; public slots: void slotChangeZoom(int value, bool zoomOnMouse); void zoneUpdated(const QPoint &zone); /* @brief Favorite effects have changed, reload model for context menu */ void updateEffectFavorites(); /* @brief Favorite transitions have changed, reload model for context menu */ void updateTransitionFavorites(); private slots: void slotUngrabHack(); private: ThumbnailProvider *m_thumbnailer; TimelineController *m_proxy; static const int comboScale[]; std::shared_ptr m_transitionModel; std::unique_ptr m_transitionProxyModel; std::shared_ptr m_effectsModel; std::unique_ptr m_effectsProxyModel; std::unique_ptr m_sortModel; /* @brief Returns an alphabetically sorted list of favorite effects or transitions */ const QStringList sortedItems(const QStringList &items, bool isTransition); signals: void focusProjectMonitor(); void zoneMoved(const QPoint &zone); }; #endif diff --git a/tests/modeltest.cpp b/tests/modeltest.cpp index 0013e9e38..de6d30b0b 100644 --- a/tests/modeltest.cpp +++ b/tests/modeltest.cpp @@ -1,2190 +1,2179 @@ #include "test_utils.hpp" using namespace fakeit; std::default_random_engine g(42); Mlt::Profile profile_model; TEST_CASE("Basic creation/deletion of a track", "[TrackModel]") { Logger::clear(); auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); int id1 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->getTrackPosition(id1) == 0); // In the current implementation, when a track is added/removed, the model is notified with _resetView Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id2 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); REQUIRE(timeline->getTrackPosition(id2) == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id3 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); REQUIRE(timeline->getTrackPosition(id3) == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id4; REQUIRE(timeline->requestTrackInsertion(1, id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 4); REQUIRE(timeline->getTrackPosition(id1) == 0); REQUIRE(timeline->getTrackPosition(id4) == 1); REQUIRE(timeline->getTrackPosition(id2) == 2); REQUIRE(timeline->getTrackPosition(id3) == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); // Test deletion REQUIRE(timeline->requestTrackDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 0); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); SECTION("Delete a track with groups") { int tid1, tid2; REQUIRE(timeline->requestTrackInsertion(-1, tid1)); REQUIRE(timeline->requestTrackInsertion(-1, tid2)); REQUIRE(timeline->checkConsistency()); QString binId = createProducer(profile_model, "red", binModel); int length = 20; int cid1, cid2, cid3, cid4; REQUIRE(timeline->requestClipInsertion(binId, tid1, 2, cid1)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 0, cid2)); REQUIRE(timeline->requestClipInsertion(binId, tid2, length, cid3)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 2 * length, cid4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 4); REQUIRE(timeline->getTracksCount() == 2); auto g1 = std::unordered_set({cid1, cid3}); auto g2 = std::unordered_set({cid2, cid4}); auto g3 = std::unordered_set({cid1, cid4}); REQUIRE(timeline->requestClipsGroup(g1)); REQUIRE(timeline->requestClipsGroup(g2)); REQUIRE(timeline->requestClipsGroup(g3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->checkConsistency()); } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Basic creation/deletion of a clip", "[ClipModel]") { Logger::clear(); auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); std::shared_ptr timeline = TimelineItemModel::construct(&profile_model, guideModel, undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "green", binModel); REQUIRE(timeline->getClipsCount() == 0); int id1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->checkConsistency()); int id2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->checkConsistency()); int id3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->checkConsistency()); // Test deletion REQUIRE(timeline->requestItemDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->requestItemDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->requestItemDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 0); binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Clip manipulation", "[ClipModel]") { Logger::clear(); auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); Fake(Method(timMock, _beginInsertRows)); Fake(Method(timMock, _beginRemoveRows)); Fake(Method(timMock, _endInsertRows)); Fake(Method(timMock, _endRemoveRows)); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducer(profile_model, "green", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); Verify(Method(timMock, _resetView)).Exactly(3_Times); RESET(timMock); // for testing purposes, we make sure the clip will behave as regular clips // (ie their size is fixed, we cannot resize them past their original size) timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; SECTION("Insert a clip in a track and change track") { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(timeline->getClipPosition(cid1) == -1); int pos = 10; REQUIRE(timeline->requestClipMove(cid1, tid1, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); // Check that the model was correctly notified CHECK_INSERT(Once); pos = 1; REQUIRE(timeline->requestClipMove(cid1, tid2, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); CHECK_MOVE(Once); // Check conflicts int pos2 = binModel->getClipByBinID(binId)->frameDuration(); REQUIRE(timeline->requestClipMove(cid2, tid1, pos2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 + 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 - 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); } int length = binModel->getClipByBinID(binId)->frameDuration(); SECTION("Insert consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == length); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_INSERT(Once); } SECTION("Resize orphan clip") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); auto inOut = std::pair{0, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 10, false) == -1); REQUIRE(timeline->requestItemResize(cid2, length + 1, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 2, false) == 2); REQUIRE(timeline->checkConsistency()); inOut = std::pair{3, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 2); REQUIRE(timeline->requestItemResize(cid2, length, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 2); CAPTURE(timeline->m_allClips[cid2]->m_producer->get_in()); REQUIRE(timeline->requestItemResize(cid2, length - 2, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestItemResize(cid2, length - 3, true) == length - 3); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == length - 3); } SECTION("Resize inserted clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == 5); REQUIRE(timeline->getClipPosition(cid1) == 0); CHECK_RESIZE(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 6, true) == -1); REQUIRE(timeline->requestItemResize(cid1, 6, false) == -1); REQUIRE(timeline->checkConsistency()); NO_OTHERS(); REQUIRE(timeline->requestItemResize(cid2, length - 5, false) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPosition(cid2) == 10); CHECK_RESIZE(Once); REQUIRE(timeline->requestItemResize(cid1, 10, true) == 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_RESIZE(Once); } SECTION("Change track of resized clips") { // // REQUIRE(timeline->allowClipMove(cid2, tid1, 5)); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); // // REQUIRE(timeline->allowClipMove(cid1, tid2, 10)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->requestItemResize(cid1, 5, false) == 5); REQUIRE(timeline->checkConsistency()); // // REQUIRE(timeline->allowClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); } SECTION("Clip Move") { REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid1, tid1, 5 + length)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 5); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 3 + length)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state(); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 0); }; state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, length - 5)); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestClipMove(cid1, tid1, length - 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); } SECTION("Move and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestItemResize(cid1, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 2); }; state(); // try to resize past the left end REQUIRE(timeline->requestItemResize(cid1, length, false) == -1); state(); REQUIRE(timeline->requestItemResize(cid1, length - 4, true) == length - 4); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); REQUIRE(timeline->requestItemResize(cid2, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4 + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state2(); // the gap between the two clips is 1 frame, we try to resize them by 2 frames REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state2(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state2(); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state3(); // Now the gap is 0 frames, the resize should still fail REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state3(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state3(); // We move cid1 out of the way REQUIRE(timeline->requestClipMove(cid1, tid2, 0)); // now resize should work REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == length - 2); REQUIRE(timeline->requestItemResize(cid2, length, false) == length); REQUIRE(timeline->checkConsistency()); } SECTION("Group move") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, length + 3)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 5)); REQUIRE(timeline->requestClipMove(cid4, tid2, 4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid4, tid2, 9)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); }; state(); // grouping REQUIRE(timeline->requestClipsGroup({cid1, cid3})); REQUIRE(timeline->requestClipsGroup({cid1, cid4})); // move left is now forbidden, because clip1 is at position 0 REQUIRE_FALSE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); state(); // this move is impossible, because clip1 runs into clip2 REQUIRE_FALSE(timeline->requestClipMove(cid4, tid2, 9)); state(); // this move is possible REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 8)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid3) == 0); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 3); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 8); REQUIRE(timeline->getClipPosition(cid4) == 7); }; state1(); // this move is possible REQUIRE(timeline->requestClipMove(cid1, tid2, 8)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 2); REQUIRE(timeline->getTrackClipsCount(tid3) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid2); REQUIRE(timeline->getClipTrackId(cid4) == tid3); REQUIRE(timeline->getClipPosition(cid1) == 8); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5 + 8); REQUIRE(timeline->getClipPosition(cid4) == 4 + 8); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, 3)); state1(); } SECTION("Group move consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 7)); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + length)); REQUIRE(timeline->requestClipMove(cid3, tid1, 7 + 2 * length)); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 3 * length)); REQUIRE(timeline->requestClipsGroup({cid1, cid2, cid3, cid4})); auto state = [&](int tid, int start) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid) == 4); int i = 0; for (int cid : std::vector({cid1, cid2, cid3, cid4})) { REQUIRE(timeline->getClipTrackId(cid) == tid); REQUIRE(timeline->getClipPosition(cid) == start + i * length); REQUIRE(timeline->getClipPlaytime(cid) == length); i++; } }; state(tid1, 7); auto check_undo = [&](int target, int tid, int oldTid) { state(tid, target); undoStack->undo(); state(oldTid, 7); undoStack->redo(); state(tid, target); undoStack->undo(); state(oldTid, 7); }; REQUIRE(timeline->requestClipMove(cid1, tid1, 6)); qDebug() << "state1"; state(tid1, 6); undoStack->undo(); state(tid1, 7); undoStack->redo(); state(tid1, 6); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); qDebug() << "state2"; state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->redo(); state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->undo(); state(tid1, 7); REQUIRE(timeline->requestClipMove(cid3, tid1, 1 + 2 * length)); qDebug() << "state3"; check_undo(1, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 4 + 3 * length)); qDebug() << "state4"; check_undo(4, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 11 + 3 * length)); qDebug() << "state5"; check_undo(11, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 13 + length)); qDebug() << "state6"; check_undo(13, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 20)); qDebug() << "state7"; check_undo(20, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 4 * length)); qDebug() << "state8"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + 2 * length)); qDebug() << "state9"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 7 + length)); qDebug() << "state10"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid2, 8 + length)); qDebug() << "state11"; check_undo(8, tid2, tid1); } SECTION("Group move to unavailable track") { REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); REQUIRE(timeline->requestClipMove(cid2, tid2, 12)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 12); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 10)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 100)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid3, 100)); state(); } SECTION("Group move with non-consecutive track ids") { int tid5 = TrackModel::construct(timeline); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); Q_UNUSED(cid6); int tid6 = TrackModel::construct(timeline); REQUIRE(tid5 + 1 != tid6); REQUIRE(timeline->requestClipMove(cid1, tid5, 10)); REQUIRE(timeline->requestClipMove(cid2, tid5, length + 10)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&](int t) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(t) == 2); REQUIRE(timeline->getClipTrackId(cid1) == t); REQUIRE(timeline->getClipTrackId(cid2) == t); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 10 + length); }; state(tid5); REQUIRE(timeline->requestClipMove(cid1, tid6, 10)); state(tid6); } SECTION("Creation and movement of AV groups") { int tid6b = TrackModel::construct(timeline, -1, -1, QString(), true); int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true); int tid5 = TrackModel::construct(timeline); int tid5b = TrackModel::construct(timeline); QString binId3 = createProducerWithSound(profile_model, binModel); int cid6 = -1; REQUIRE(timeline->requestClipInsertion(binId3, tid5, 3, cid6, true, true, false)); int cid7 = timeline->m_groups->getSplitPartner(cid6); auto check_group = [&]() { // we check that the av group was correctly created REQUIRE(timeline->getGroupElements(cid6) == std::unordered_set({cid6, cid7})); int g1 = timeline->m_groups->getDirectAncestor(cid6); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid6, cid7})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); }; auto state = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5); REQUIRE(timeline->getClipTrackId(cid7) == tid6); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); check_group(); }; state(3); // simple translation on the right REQUIRE(timeline->requestClipMove(cid6, tid5, 10, true, true, true)); state(10); undoStack->undo(); state(3); undoStack->redo(); state(10); // simple translation on the left, moving the audio clip this time REQUIRE(timeline->requestClipMove(cid7, tid6, 1, true, true, true)); state(1); undoStack->undo(); state(10); undoStack->redo(); state(1); // change track, moving video REQUIRE(timeline->requestClipMove(cid6, tid5b, 7, true, true, true)); auto state2 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5b) == 1); REQUIRE(timeline->getTrackClipsCount(tid6b) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5b); REQUIRE(timeline->getClipTrackId(cid7) == tid6b); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); check_group(); }; state2(7); undoStack->undo(); state(1); undoStack->redo(); state2(7); // change track, moving audio REQUIRE(timeline->requestClipMove(cid7, tid6b, 2, true, true, true)); state2(2); undoStack->undo(); state2(7); undoStack->redo(); state2(2); undoStack->undo(); undoStack->undo(); state(1); } - SECTION("Clip copy") + SECTION("Clip clone") { int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int l = timeline->getClipPlaytime(cid6); REQUIRE(timeline->requestItemResize(cid6, l - 3, true, true, -1) == l - 3); REQUIRE(timeline->requestItemResize(cid6, l - 7, false, true, -1) == l - 7); int newId; std::function undo = []() { return true; }; std::function redo = []() { return true; }; - REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo)); + REQUIRE(TimelineFunctions::cloneClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo)); REQUIRE(timeline->m_allClips[cid6]->binId() == timeline->m_allClips[newId]->binId()); // TODO check effects } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Check id unicity", "[ClipModel]") { Logger::clear(); auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); std::vector track_ids; std::unordered_set all_ids; std::bernoulli_distribution coin(0.5); const int nbr = 20; for (int i = 0; i < nbr; i++) { if (coin(g)) { int tid = TrackModel::construct(timeline); REQUIRE(all_ids.count(tid) == 0); all_ids.insert(tid); track_ids.push_back(tid); REQUIRE(timeline->getTracksCount() == track_ids.size()); } else { int cid = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(all_ids.count(cid) == 0); all_ids.insert(cid); REQUIRE(timeline->getClipsCount() == all_ids.size() - track_ids.size()); } } REQUIRE(timeline->checkConsistency()); REQUIRE(all_ids.size() == nbr); REQUIRE(all_ids.size() != track_ids.size()); binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Undo and Redo", "[ClipModel]") { Logger::clear(); auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; int length = 20; int nclips = timeline->m_allClips.size(); SECTION("requestCreateClip") { // an invalid clip id shouldn't get created { int temp; Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::VideoOnly, 1., undo, redo)); } auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId3, cid3, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId4, cid4, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == -1); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } SECTION("requestInsertClip") { auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; REQUIRE(timeline->requestClipInsertion(binId3, tid1, 12, cid3, true)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; REQUIRE(timeline->requestClipInsertion(binId4, tid2, 17, cid4, true)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid4) == 17); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } int init_index = undoStack->index(); SECTION("Basic move undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); // Move on same track does not trigger insert/remove row CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); REQUIRE(timeline->requestClipMove(cid1, tid1, 2 * length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(undoStack->index() == init_index); CHECK_REMOVE(Once); } SECTION("Basic resize orphan clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->redo(); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->undo(); REQUIRE(undoStack->index() == init_index); REQUIRE(timeline->getClipPlaytime(cid2) == length); } SECTION("Basic resize inserted clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); auto check = [&](int pos, int l) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPlaytime(cid2) == l); REQUIRE(timeline->getClipPosition(cid2) == pos); }; REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); INFO("Test 1"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); INFO("Test 2"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); INFO("Test 3"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); INFO("Test 4"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 5"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->redo(); INFO("Test 6"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 7"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->undo(); INFO("Test 8"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); } SECTION("Clip Insertion Undo") { QString binId3 = createProducer(profile_model, "red", binModel); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int cid3; REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 5, cid3)); state1(); REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 6, cid3)); state1(); REQUIRE(timeline->requestClipInsertion(binId3, tid1, 5 + length, cid3)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 5 + length); REQUIRE(timeline->m_allClips[cid3]->isValid()); REQUIRE(undoStack->index() == init_index + 2); }; state2(); REQUIRE(timeline->requestClipMove(cid3, tid1, 10 + length)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 3); }; state3(); REQUIRE(timeline->requestItemResize(cid3, 1, true) == 1); auto state4 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPlaytime(cid3) == 1); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 4); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); } SECTION("Clip Deletion undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int nbClips = timeline->getClipsCount(); REQUIRE(timeline->requestItemDeletion(cid1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipsCount() == nbClips - 1); REQUIRE(undoStack->index() == init_index + 2); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->undo(); state1(); } SECTION("Track insertion undo") { std::map orig_trackPositions, final_trackPositions; for (const auto &it : timeline->m_iteratorTable) { int track = it.first; int pos = timeline->getTrackPosition(track); orig_trackPositions[track] = pos; if (pos >= 1) pos++; final_trackPositions[track] = pos; } auto checkPositions = [&](const std::map &pos) { for (const auto &p : pos) { REQUIRE(timeline->getTrackPosition(p.first) == p.second); } }; checkPositions(orig_trackPositions); int new_tid; REQUIRE(timeline->requestTrackInsertion(1, new_tid)); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); undoStack->redo(); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); } SECTION("Track deletion undo") { int nb_clips = timeline->getClipsCount(); int nb_tracks = timeline->getTracksCount(); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipsCount() == nb_clips); REQUIRE(timeline->getTracksCount() == nb_tracks); }; state1(); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); undoStack->redo(); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); } int clipCount = timeline->m_allClips.size(); SECTION("Clip creation and resize") { int cid6; auto state0 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount); REQUIRE(timeline->checkConsistency()); }; state0(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId, cid6, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } int l = timeline->getClipPlaytime(cid6); auto state1 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l); }; state1(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestItemResize(cid6, l - 5, true, true, undo, redo, false)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state2(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestClipMove(cid6, tid1, 7, true, true, undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state3 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 7); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state3(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestItemResize(cid6, l - 6, false, true, undo, redo, false)); pCore->pushUndo(undo, redo, QString()); } auto state4 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 8); REQUIRE(timeline->getClipPlaytime(cid6) == l - 6); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Snapping", "[Snapping]") { Logger::clear(); auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel, 50); QString binId2 = createProducer(profile_model, "blue", binModel); int tid1 = TrackModel::construct(timeline); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; int length = timeline->getClipPlaytime(cid1); int length2 = timeline->getClipPlaytime(cid2); SECTION("getBlankSizeNearClip") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 25 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 15); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 15); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 10 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 0); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); } SECTION("Snap move to a single clip") { int beg = 30; // in the absence of other clips, a valid move shouldn't be modified for (int snap = -1; snap <= 5; ++snap) { REQUIRE(timeline->suggestClipMove(cid2, tid2, beg, -1, snap) == beg); REQUIRE(timeline->suggestClipMove(cid2, tid2, beg + length, -1, snap) == beg + length); REQUIRE(timeline->checkConsistency()); } // We add a clip in first track to create snap points REQUIRE(timeline->requestClipMove(cid1, tid1, beg)); // Now a clip in second track should snap to beginning auto check_snap = [&](int pos, int perturb, int snap) { if (snap >= perturb) { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, -1, snap) == pos); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, -1, snap) == pos); } else { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, -1, snap) == pos + perturb); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, -1, snap) == pos - perturb); } }; for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { // snap to beginning check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); // snap to end check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); } } // Same test, but now clip is moved in position 0 first REQUIRE(timeline->requestClipMove(cid2, tid2, 0)); for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { // snap to beginning check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); // snap to end check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); } } } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Advanced trimming operations", "[Trimming]") { Logger::clear(); auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(&profile_model, undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducerWithSound(profile_model, binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); // Add an audio track int tid4 = TrackModel::construct(timeline, -1, -1, QString(), true); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid5 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid7 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int audio1 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio2 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; timeline->m_allClips[cid5]->m_endlessResize = false; timeline->m_allClips[cid6]->m_endlessResize = false; timeline->m_allClips[cid7]->m_endlessResize = false; SECTION("Clip cutting") { // Trivial split REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestItemResize(cid2, l - 3, true) == l - 3); REQUIRE(timeline->requestItemResize(cid2, l - 5, false) == l - 5); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, l + l - 5)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == l - 5); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == l - 4); }; state(); // require cut position outside the clip REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); // can't cut on edges either REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l + l - 5)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 9); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == 5); REQUIRE(timeline->getClipPtr(splitted)->getIn() == 6); REQUIRE(timeline->getClipPtr(splitted)->getOut() == l - 4); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } SECTION("Cut and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); int l = timeline->getClipPlaytime(cid1); timeline->m_allClips[cid1]->m_endlessResize = false; auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); }; state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid1, 9)); int splitted = timeline->getClipByPosition(tid1, 10); timeline->m_allClips[splitted]->m_endlessResize = false; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 9); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid2, 9, true, true)); REQUIRE(timeline->requestItemResize(splitted, l - 3, true, true) == -1); REQUIRE(timeline->requestItemResize(splitted, l, false, true) == l); REQUIRE(timeline->requestItemResize(cid1, 5, false, true) == -1); REQUIRE(timeline->requestItemResize(cid1, l, true, true) == l); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid2); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(splitted) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 5); }; state3(); undoStack->undo(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Clip cutting 2") { // More complex group structure split split int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * l)); REQUIRE(timeline->requestClipMove(cid4, tid2, 0)); REQUIRE(timeline->requestClipMove(cid5, tid2, l)); REQUIRE(timeline->requestClipMove(cid6, tid2, 2 * l)); REQUIRE(timeline->requestClipMove(cid7, tid1, 200)); int gid1 = timeline->requestClipsGroup(std::unordered_set({cid1, cid4}), true, GroupType::Normal); int gid2 = timeline->requestClipsGroup(std::unordered_set({cid2, cid5}), true, GroupType::Normal); int gid3 = timeline->requestClipsGroup(std::unordered_set({cid3, cid6}), true, GroupType::Normal); int gid4 = timeline->requestClipsGroup(std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7}), true, GroupType::Normal); auto state = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->m_groups->getDirectChildren(gid1) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid1, gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7})); }; state(); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 2 * l)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); int splitted2 = timeline->getClipByPosition(tid2, l + 5); REQUIRE(splitted != splitted2); auto check_groups = [&]() { REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({splitted, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({splitted, splitted2, cid3, cid6, cid7})); int g1b = timeline->m_groups->m_upLink[cid1]; int g2b = timeline->m_groups->m_upLink[cid2]; int g4b = timeline->m_groups->getRootId(cid1); REQUIRE(timeline->m_groups->getDirectChildren(g1b) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(g2b) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(g4b) == std::unordered_set({g1b, g2b})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid4, cid5})); }; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid1, l + 4 + 10, true, true)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10, true, true)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid3 ? tid1 : tid2)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid6 ? tid2 : tid3)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 210); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state3(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Simple audio split") { int l = timeline->getClipPlaytime(audio1); REQUIRE(timeline->requestClipMove(audio1, tid1, 3)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1})); }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4)); int splitted1 = timeline->getClipByPosition(tid4, 3); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPosition(splitted1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid4); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid4) == 1); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1})); int g1 = timeline->m_groups->getDirectAncestor(audio1); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); // We also make sure that clips that are audio only cannot be further splitted REQUIRE(timeline->requestClipMove(cid1, tid1, l + 30)); // This is a color clip, shouldn't be splittable REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2)); // Check we cannot split audio on a video track REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2)); } SECTION("Split audio on a selection") { int l = timeline->getClipPlaytime(audio2); REQUIRE(timeline->requestClipMove(audio1, tid1, 0)); REQUIRE(timeline->requestClipMove(audio2, tid1, l)); REQUIRE(timeline->requestClipMove(audio3, tid1, 2 * l)); std::unordered_set selection{audio1, audio3, audio2}; - REQUIRE(timeline->requestClipsGroup(selection, false, GroupType::Selection)); + timeline->requestSetSelection(selection); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, audio2, audio3})); - int sel = timeline->m_temporarySelectionGroup; - // check that selection is preserved - REQUIRE(sel != -1); - REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); + REQUIRE(timeline->getCurrentSelection() == std::unordered_set({audio1, audio3, audio2})); + }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4)); int splitted1 = timeline->getClipByPosition(tid4, 0); int splitted2 = timeline->getClipByPosition(tid4, l); int splitted3 = timeline->getClipByPosition(tid4, 2 * l); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPlaytime(splitted2) == l); REQUIRE(timeline->getClipPlaytime(splitted3) == l); REQUIRE(timeline->getClipPosition(splitted1) == 0); REQUIRE(timeline->getClipPosition(splitted2) == l); REQUIRE(timeline->getClipPosition(splitted3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid4); REQUIRE(timeline->getClipTrackId(splitted2) == tid4); REQUIRE(timeline->getClipTrackId(splitted3) == tid4); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid4) == 3); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3})); - - int sel = timeline->m_temporarySelectionGroup; - // check that selection is preserved - REQUIRE(sel != -1); - REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); - - REQUIRE(timeline->m_groups->getRootId(audio1) == sel); - REQUIRE(timeline->m_groups->getDirectChildren(sel).size() == 3); - REQUIRE(timeline->m_groups->getLeaves(sel).size() == 6); + REQUIRE(timeline->getCurrentSelection() == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3})); int g1 = timeline->m_groups->getDirectAncestor(audio1); int g2 = timeline->m_groups->getDirectAncestor(audio2); int g3 = timeline->m_groups->getDirectAncestor(audio3); - REQUIRE(timeline->m_groups->getDirectChildren(sel) == std::unordered_set({g1, g2, g3})); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({audio2, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set({audio3, splitted3})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } SECTION("Cut should preserve AV groups") { QString binId3 = createProducerWithSound(profile_model, binModel); int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true); int tid5 = TrackModel::construct(timeline); int cid6 = -1; int pos = 3; REQUIRE(timeline->requestClipInsertion(binId3, tid5, pos, cid6, true, true, false)); int cid7 = timeline->m_groups->getSplitPartner(cid6); int l = timeline->getClipPlaytime(cid6); REQUIRE(l >= 10); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5); REQUIRE(timeline->getClipTrackId(cid7) == tid6); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); // we check that the av group was correctly created REQUIRE(timeline->getGroupElements(cid6) == std::unordered_set({cid6, cid7})); int g1 = timeline->m_groups->getDirectAncestor(cid6); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid6, cid7})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); }; state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid6, pos + 4)); int cid8 = timeline->getClipByPosition(tid5, pos + 5); int cid9 = timeline->getClipByPosition(tid6, pos + 5); REQUIRE(cid8 >= 0); REQUIRE(cid9 >= 0); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 2); REQUIRE(timeline->getTrackClipsCount(tid6) == 2); REQUIRE(timeline->getClipTrackId(cid6) == tid5); REQUIRE(timeline->getClipTrackId(cid7) == tid6); REQUIRE(timeline->getClipTrackId(cid8) == tid5); REQUIRE(timeline->getClipTrackId(cid9) == tid6); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPosition(cid8) == pos + 4); REQUIRE(timeline->getClipPosition(cid9) == pos + 4); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); REQUIRE(timeline->getClipPtr(cid8)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid9)->clipState() == PlaylistState::AudioOnly); // original AV group REQUIRE(timeline->getGroupElements(cid6) == std::unordered_set({cid6, cid7})); int g1 = timeline->m_groups->getDirectAncestor(cid6); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid6, cid7})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); // new AV group REQUIRE(timeline->getGroupElements(cid8) == std::unordered_set({cid8, cid9})); int g2 = timeline->m_groups->getDirectAncestor(cid8); REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({cid8, cid9})); REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); }