diff --git a/src/kdenliveui.rc b/src/kdenliveui.rc index ba91621a9..091ecf481 100644 --- a/src/kdenliveui.rc +++ b/src/kdenliveui.rc @@ -1,256 +1,257 @@ - + Project Generators Tool Clip Markers Extract Audio Clip Jobs Transcode Clip in Timeline Timeline Selection Insertion Removal Timeline Preview Current clip + Guides Space Tracks Add Effect Monitor Go To Seek Current Monitor Overlay Monitor config View Save Layout As Timeline Toolbar Extra Toolbar diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0b0118ba5..adc182796 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,3751 +1,3762 @@ /*************************************************************************** * 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 "jobs/transcodeclipjob.h" #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 #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("Windows")}; if (incompatibleStyles.contains(desktopStyle, Qt::CaseInsensitive)) { if (availableStyles.contains(QStringLiteral("breeze"), Qt::CaseInsensitive)) { // Auto switch to Breeze theme KdenliveSettings::setWidgetstyle(QStringLiteral("Breeze")); QApplication::setStyle(QStyleFactory::create(QStringLiteral("Breeze"))); } else if (availableStyles.contains(QStringLiteral("fusion"), Qt::CaseInsensitive)) { KdenliveSettings::setWidgetstyle(QStringLiteral("Fusion")); QApplication::setStyle(QStyleFactory::create(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::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); // Screen grab widget QWidget *grabWidget = new QWidget(this); QVBoxLayout *grabLayout = new QVBoxLayout; grabWidget->setLayout(grabLayout); QToolBar *recToolbar = new QToolBar(grabWidget); grabLayout->addWidget(recToolbar); grabLayout->addStretch(10); // Check number of monitors for FFmpeg screen capture int screens = QApplication::desktop()->screenCount(); if (screens > 1) { QComboBox *screenCombo = new QComboBox(recToolbar); for (int ix = 0; ix < screens; ix++) { screenCombo->addItem(i18n("Monitor %1", ix)); } connect(screenCombo, static_cast(&QComboBox::currentIndexChanged), m_clipMonitor, &Monitor::slotSetScreen); recToolbar->addWidget(screenCombo); // Update screen grab monitor choice in case we changed from fullscreen screenCombo->setEnabled(KdenliveSettings::grab_capture_type() == 0); } QAction *recAction = m_clipMonitor->recAction(); addAction(QStringLiteral("screengrab_record"), recAction); recToolbar->addAction(recAction); QAction *recConfig = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Recording"), this); recToolbar->addAction(recConfig); connect(recConfig, &QAction::triggered, [&]() { pCore->showConfigDialog(4, 0); }); QDockWidget *screenGrabDock = addDock(i18n("Screen Grab"), QStringLiteral("screengrab"), grabWidget); // 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 screenGrabDock->close(); 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_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, [&] () { // Don't raise effect stack on clip bin in case it is docked with bin or clip monitor // 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("Compositions"), 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(); // 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 < QT_VERSION_CHECK(5, 23, 0)) || defined(Q_OS_WIN) // Not required anymore with auto colored icons since KF5 5.23 if (m_themeInitialized && useDarkIcons != m_isDarkTheme) { QIcon::setThemeName(useDarkIcons ? QStringLiteral("breeze-dark") : QStringLiteral("breeze")); 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() == 1); QAction *splitView2 = new QAction(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")), i18n("Split Audio Tracks (reverse)"), this); addAction(QStringLiteral("timeline_view_split_reverse"), splitView2); splitView2->setData(QVariant::fromValue(2)); splitView2->setCheckable(true); splitView2->setChecked(KdenliveSettings::audiotracksbelow() == 2); 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() == 0); auto *clipTypeGroup = new QActionGroup(this); clipTypeGroup->addAction(mixedView); clipTypeGroup->addAction(splitView); clipTypeGroup->addAction(splitView2); 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); tlsettings->addAction(splitView2); QToolButton *timelineSett = new QToolButton(this); timelineSett->setPopupMode(QToolButton::InstantPopup); timelineSett->setMenu(tlsettings); timelineSett->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); auto *tlButtonAction = new QWidgetAction(this); tlButtonAction->setDefaultWidget(timelineSett); tlButtonAction->setText(i18n("Track menu")); addAction(QStringLiteral("timeline_settings"), tlButtonAction); 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"))); - - + QAction *resetAction = new QAction(QIcon::fromTheme(QStringLiteral("reload")), i18n("Reset configuration"), this); addAction(QStringLiteral("reset_config"), resetAction); connect(resetAction, &QAction::triggered, [&]() { slotRestart(true); }); 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 *switchEnable = addAction(QStringLiteral("clip_switch"), i18n("Disable Clip"), this, SLOT(slotSwitchClip()), + QIcon(), QKeySequence(), clipActionCategory); + // "S" will be handled specifically to change the action name depending on current selection + switchEnable->setData('W'); + switchEnable->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("edit_item_speed"), i18n("Change Speed"), this, SLOT(slotEditItemSpeed()), QIcon::fromTheme(QStringLiteral("speedometer")), 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); 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); 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"))); 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); connect(this, &MainWindow::enableUndo, [this, undo] (bool enable) { if (enable) { enable = m_commandStack->activeStack()->canUndo(); } undo->setEnabled(enable); }); QAction *redo = KStandardAction::redo(m_commandStack, SLOT(redo()), actionCollection()); redo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canRedoChanged, redo, &QAction::setEnabled); connect(this, &MainWindow::enableUndo, [this, redo] (bool enable) { if (enable) { enable = m_commandStack->activeStack()->canRedo(); } redo->setEnabled(enable); }); 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); 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("switch_active_target"), i18n("Toggle Track Active"), pCore->projectManager(), SLOT(slotSwitchTrackActive()), QIcon(), Qt::Key_A); addAction(QStringLiteral("add_project_note"), i18n("Add Project Note"), pCore->projectManager(), SLOT(slotAddProjectNote()), QIcon::fromTheme(QStringLiteral("bookmark"))); pCore->bin()->setupMenu(); // 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; //Define default video location for first run KRecentDirs::add(QStringLiteral(":KdenliveClipFolder"), QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)); // 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("previewparameters")) != w->proxyParams() || project->getDocumentProperty(QStringLiteral("previewextension")) != w->proxyExtension()) { modified = true; project->setDocumentProperty(QStringLiteral("previewparameters"), w->previewParams()); project->setDocumentProperty(QStringLiteral("previewextension"), w->previewExtension()); slotClearPreviewRender(false); } 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); } } else 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); m_renderWidget->setGuides(project->getGuideModel()); 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->currentDoc()->getGuideModel()); 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) { m_renderWidget->setGuides(pCore->currentDoc()->getGuideModel()); } } 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(bool clean) { if (clean) { if (KMessageBox::questionYesNo(this, i18n("This will delete Kdenlive's configuration file and restart the application. Do you want to proceed?")) != KMessageBox::Yes) { return; } } m_exitCode = clean ? EXIT_CLEAN_RESTART : 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(); } 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()) { int selectedClip = getMainTimeline()->controller()->getMainSelectedItem(); if (selectedClip > -1) { getMainTimeline()->controller()->addMarker(selectedClip); 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()) { int selectedClip = getMainTimeline()->controller()->getMainSelectedItem(); if (selectedClip > -1) { getMainTimeline()->controller()->deleteMarker(selectedClip); return; } } 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()) { int selectedClip = getMainTimeline()->controller()->getMainSelectedItem(); if (selectedClip > -1) { getMainTimeline()->controller()->deleteAllMarkers(selectedClip); return; } } 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()) { int selectedClip = getMainTimeline()->controller()->getMainSelectedItem(); if (selectedClip > -1) { getMainTimeline()->controller()->editMarker(selectedClip); return; } } 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 { int selectedClip = getMainTimeline()->controller()->getMainSelectedItem(); if (selectedClip == -1) { // Add timeline guide getMainTimeline()->controller()->switchGuide(); } else { // Add marker to main clip getMainTimeline()->controller()->addQuickMarker(selectedClip); } } } 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); getCurrentTimeline()->controller()->addTrack(-1); } void MainWindow::slotDeleteTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getCurrentTimeline()->controller()->deleteTrack(-1); } void MainWindow::slotSelectTrack() { getCurrentTimeline()->controller()->selectCurrentTrack(); } void MainWindow::slotSelectAllTracks() { getCurrentTimeline()->controller()->selectAll(); } void MainWindow::slotUnselectAllTracks() { getCurrentTimeline()->model()->requestClearSelection(); } void MainWindow::slotEditGuide() { getCurrentTimeline()->controller()->editGuide(); } void MainWindow::slotDeleteGuide() { 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) { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(activeMonitor); } } 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) { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(activeMonitor); } } 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(bool resetZones) { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->clearPreviewRange(resetZones); } } 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() { getCurrentTimeline()->controller()->editItemDuration(); } void MainWindow::slotAddProjectClip(const QUrl &url, const QString &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() { m_timelineTabs->fitZoom(); } 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::slotSwitchClip() +{ + getMainTimeline()->controller()->switchEnableState(); +} + 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); 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; } // transcoders 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 << 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)); } connect(a, &QAction::triggered, [&, a]() { QStringList transcodeData = a->data().toStringList(); pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), -1, QString(), transcodeData.first()); }); if (transList.count() > 2 && transList.at(2) == 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_ASSERT(!urls.isEmpty()); QString params; QString desc; ClipTranscode *d = new ClipTranscode(urls, params, QStringList(), desc, pCore->bin()->getCurrentFolder()); connect(d, &ClipTranscode::addClip, this, &MainWindow::slotAddProjectClip); d->show(); } void MainWindow::slotTranscodeClip() { 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())); if (xmlDoc.isNull()) { KMessageBox::error(this, i18n("Project file could not be saved for archiving.")); return; } 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(); // Monitor refresh is necessary if (pCore->monitorManager()->isActive(Kdenlive::ClipMonitor)) { m_clipMonitorDock->raise(); } else { m_projectMonitorDock->raise(); } } 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 + return; 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()); } pCore->currentDoc()->setModified(); } 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::slotEditItemSpeed() { TimelineWidget *current = getCurrentTimeline(); if (current) { current->controller()->changeItemSpeed(-1, -1); } } 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(); } #ifdef DEBUG_MAINW #undef DEBUG_MAINW #endif diff --git a/src/mainwindow.h b/src/mainwindow.h index 7423ce7b5..2e21bc051 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,492 +1,493 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bin/bin.h" #include "definitions.h" #include "dvdwizard/dvdwizard.h" #include "gentime.h" #include "kdenlive_debug.h" #include "kdenlivecore_export.h" #include "statusbarmessagelabel.h" class AssetPanel; class AudioGraphSpectrum; class EffectStackView2; class EffectBasket; class EffectListWidget; class TransitionListWidget; class EffectStackView; class KIconLoader; class KdenliveDoc; class Monitor; class Render; class RenderWidget; class TimelineTabs; class TimelineWidget; class Transition; class MltErrorEvent : public QEvent { public: explicit MltErrorEvent(QString message) : QEvent(QEvent::User) , m_message(std::move(message)) { } QString message() const { return m_message; } private: QString m_message; }; class /*KDENLIVECORE_EXPORT*/ MainWindow : public KXmlGuiWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); /** @brief Initialises the main window. * @param MltPath (optional) path to MLT environment * @param Url (optional) file to open * @param clipsToLoad (optional) a comma separated list of clips to import in project * * 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 init(); ~MainWindow() override; /** @brief Cache for luma files thumbnails. */ static QMap m_lumacache; static QMap m_lumaFiles; /** @brief Adds an action to the action collection and stores the name. */ void addAction(const QString &name, QAction *action, const QKeySequence &shortcut = QKeySequence(), KActionCategory *category = nullptr); /** @brief Adds an action to the action collection and stores the name. */ QAction *addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon = QIcon(), const QKeySequence &shortcut = QKeySequence(), KActionCategory *category = nullptr); /** * @brief Adds a new dock widget to this window. * @param title title of the dock widget * @param objectName objectName of the dock widget (required for storing layouts) * @param widget widget to use in the dock * @param area area to which the dock should be added to * @returns the created dock widget */ QDockWidget *addDock(const QString &title, const QString &objectName, QWidget *widget, Qt::DockWidgetArea area = Qt::TopDockWidgetArea); QUndoGroup *m_commandStack; QUndoView *m_undoView; /** @brief holds info about whether movit is available on this system */ bool m_gpuAllowed; int m_exitCode{EXIT_SUCCESS}; QMap kdenliveCategoryMap; QList getExtraActions(const QString &name); /** @brief Returns true if docked widget is tabbed with another widget from its object name */ bool isTabbedWith(QDockWidget *widget, const QString &otherWidget); /** @brief Returns a ptr to the main timeline widget of the project */ TimelineWidget *getMainTimeline() const; /** @brief Returns a pointer to the current timeline */ TimelineWidget *getCurrentTimeline() const; protected: /** @brief Closes the window. * @return false if the user presses "Cancel" on a confirmation dialog or * the operation requested (starting waiting jobs or saving file) fails, * true otherwise */ bool queryClose() override; void closeEvent(QCloseEvent *) override; /** @brief Reports a message in the status bar when an error occurs. */ void customEvent(QEvent *e) override; /** @brief Stops the active monitor when the window gets hidden. */ void hideEvent(QHideEvent *e) override; /** @brief Saves the file and the window properties when saving the session. */ void saveProperties(KConfigGroup &config) override; /** @brief Restores the window and the file when a session is loaded. */ void readProperties(const KConfigGroup &config) override; void saveNewToolbarConfig() override; private: /** @brief Sets up all the actions and attaches them to the collection. */ void setupActions(); KColorSchemeManager *m_colorschemes; QDockWidget *m_projectBinDock; QDockWidget *m_effectListDock; QDockWidget *m_transitionListDock; TransitionListWidget *m_transitionList2; EffectListWidget *m_effectList2; AssetPanel *m_assetPanel{nullptr}; QDockWidget *m_effectStackDock; QDockWidget *m_clipMonitorDock; Monitor *m_clipMonitor{nullptr}; QDockWidget *m_projectMonitorDock; Monitor *m_projectMonitor{nullptr}; AudioGraphSpectrum *m_audioSpectrum; QDockWidget *m_undoViewDock; KSelectAction *m_timeFormatButton; KSelectAction *m_compositeAction; TimelineTabs *m_timelineTabs{nullptr}; /** This list holds all the scopes used in Kdenlive, allowing to manage some global settings */ QList m_gfxScopesList; KActionCategory *m_effectActions; KActionCategory *m_transitionActions; QMenu *m_effectsMenu; QMenu *m_transitionsMenu; QMenu *m_timelineContextMenu; QList m_timelineClipActions; KDualAction *m_useTimelineZone; /** Action names that can be used in the slotDoAction() slot, with their i18n() names */ QStringList m_actionNames; /** @brief Shortcut to remove the focus from any element. * * It allows to get out of e.g. text input fields and to press another * shortcut. */ QShortcut *m_shortcutRemoveFocus; RenderWidget *m_renderWidget{nullptr}; StatusBarMessageLabel *m_messageLabel{nullptr}; QList m_transitions; QAction *m_buttonAudioThumbs; QAction *m_buttonVideoThumbs; QAction *m_buttonShowMarkers; QAction *m_buttonFitZoom; QAction *m_buttonAutomaticTransition; QAction *m_normalEditTool; QAction *m_overwriteEditTool; QAction *m_insertEditTool; QAction *m_buttonSelectTool; QAction *m_buttonRazorTool; QAction *m_buttonSpacerTool; QAction *m_buttonSnap; QAction *m_saveAction; QSlider *m_zoomSlider; QAction *m_zoomIn; QAction *m_zoomOut; QAction *m_loopZone; QAction *m_playZone; QAction *m_loopClip; QAction *m_proxyClip; QString m_theme; KIconLoader *m_iconLoader; KToolBar *m_timelineToolBar; QWidget *m_timelineToolBarContainer; QLabel *m_trimLabel; /** @brief initialize startup values, return true if first run. */ bool readOptions(); void saveOptions(); void loadGenerators(); /** @brief Instantiates a "Get Hot New Stuff" dialog. * @param configFile configuration file for KNewStuff * @return number of installed items */ int getNewStuff(const QString &configFile = QString()); QStringList m_pluginFileNames; QByteArray m_timelineState; void buildDynamicActions(); void loadClipActions(); QTime m_timer; KXMLGUIClient *m_extraFactory; bool m_themeInitialized{false}; bool m_isDarkTheme{false}; EffectBasket *m_effectBasket; /** @brief Update widget style. */ void doChangeStyle(); void updateActionsToolTip(); public slots: void slotGotProgressInfo(const QString &message, int progress, MessageType type = DefaultMessage); void slotReloadEffects(const QStringList &paths); Q_SCRIPTABLE void setRenderingProgress(const QString &url, int progress); Q_SCRIPTABLE void setRenderingFinished(const QString &url, int status, const QString &error); Q_SCRIPTABLE void addProjectClip(const QString &url); Q_SCRIPTABLE void addTimelineClip(const QString &url); Q_SCRIPTABLE void addEffect(const QString &effectId); Q_SCRIPTABLE void scriptRender(const QString &url); Q_NOREPLY void exitApp(); void slotSwitchVideoThumbs(); void slotSwitchAudioThumbs(); void slotPreferences(int page = -1, int option = -1); void connectDocument(); /** @brief Reload project profile in config dialog if changed. */ void slotRefreshProfiles(); void updateDockTitleBars(bool isTopLevel = true); void configureToolbars() override; /** @brief Decreases the timeline zoom level by 1. */ void slotZoomIn(bool zoomOnMouse = false); /** @brief Increases the timeline zoom level by 1. */ void slotZoomOut(bool zoomOnMouse = false); /** @brief Enable or disable the use of timeline zone for edits. */ void slotSwitchTimelineZone(bool toggled); /** @brief Open the online services search dialog. */ void slotDownloadResources(); private slots: /** @brief Shows the shortcut dialog. */ void slotEditKeys(); void loadDockActions(); /** @brief Reflects setting changes to the GUI. */ void updateConfiguration(); void slotConnectMonitors(); void slotUpdateMousePosition(int pos); void slotUpdateProjectDuration(int pos); void slotEditProjectSettings(); void slotSwitchMarkersComments(); void slotSwitchSnap(); void slotSwitchAutomaticTransition(); void slotRenderProject(); void slotStopRenderProject(); void slotFullScreen(); /** @brief if modified is true adds "modified" to the caption and enables the save button. * (triggered by KdenliveDoc::setModified()) */ void slotUpdateDocumentState(bool modified); /** @brief Sets the timeline zoom slider to @param value. * * Also disables zoomIn and zoomOut actions if they cannot be used at the moment. */ void slotSetZoom(int value, bool zoomOnMouse = false); /** @brief Makes the timeline zoom level fit the timeline content. */ void slotFitZoom(); /** @brief Updates the zoom slider tooltip to fit @param zoomlevel. */ void slotUpdateZoomSliderToolTip(int zoomlevel); /** @brief Timeline was zoom, update slider to reflect that */ void updateZoomSlider(int value); /** @brief Displays the zoom slider tooltip. * @param zoomlevel (optional) The zoom level to show in the tooltip. * * Adopted from Dolphin (src/statusbar/dolphinstatusbar.cpp) */ void slotShowZoomSliderToolTip(int zoomlevel = -1); /** @brief Deletes item in timeline, project tree or effect stack depending on focus. */ void slotDeleteItem(); void slotAddClipMarker(); void slotDeleteClipMarker(bool allowGuideDeletion = false); void slotDeleteAllClipMarkers(); void slotEditClipMarker(); /** @brief Adds marker or guide at the current position without showing the marker dialog. * * Adds a marker if clip monitor is active, otherwise a guide. * The comment is set to the current position (therefore not dialog). * This can be useful to mark something during playback. */ void slotAddMarkerGuideQuickly(); void slotCutTimelineClip(); void slotInsertClipOverwrite(); void slotInsertClipInsert(); void slotExtractZone(); void slotLiftZone(); void slotPreviewRender(); void slotStopPreviewRender(); void slotDefinePreviewRender(); void slotRemovePreviewRender(); void slotClearPreviewRender(bool resetZones = true); void slotSelectTimelineClip(); void slotSelectTimelineTransition(); void slotDeselectTimelineClip(); void slotDeselectTimelineTransition(); void slotSelectAddTimelineClip(); void slotSelectAddTimelineTransition(); void slotAddEffect(QAction *result); void slotAddTransition(QAction *result); void slotAddProjectClip(const QUrl &url, const QString &folderInfo); void slotAddProjectClipList(const QList &urls); void slotChangeTool(QAction *action); void slotChangeEdit(QAction *action); void slotSetTool(ProjectTool tool); void slotSnapForward(); void slotSnapRewind(); void slotClipStart(); void slotClipEnd(); void slotSelectClipInTimeline(); void slotClipInTimeline(const QString &clipId, const QList &ids); void slotInsertSpace(); void slotRemoveSpace(); void slotRemoveAllSpace(); void slotAddGuide(); void slotEditGuide(); void slotDeleteGuide(); void slotDeleteAllGuides(); void slotGuidesUpdated(); void slotCopy(); void slotPaste(); void slotPasteEffects(); void slotResizeItemStart(); void slotResizeItemEnd(); void configureNotifications(); void slotInsertTrack(); void slotDeleteTrack(); /** @brief Select all clips in active track. */ void slotSelectTrack(); /** @brief Select all clips in timeline. */ void slotSelectAllTracks(); void slotUnselectAllTracks(); void slotGetNewKeyboardStuff(QComboBox *schemesList); void slotAutoTransition(); void slotRunWizard(); void slotZoneMoved(int start, int end); void slotDvdWizard(const QString &url = QString()); void slotGroupClips(); void slotUnGroupClips(); void slotEditItemDuration(); void slotClipInProjectTree(); // void slotClipToProjectTree(); void slotSplitAV(); + void slotSwitchClip(); void slotSetAudioAlignReference(); void slotAlignAudio(); void slotUpdateClipType(QAction *action); void slotUpdateTimelineView(QAction *action); void slotShowTimeline(bool show); void slotTranscode(const QStringList &urls = QStringList()); void slotTranscodeClip(); /** @brief Archive project: creates a copy of the project file with all clips in a new folder. */ void slotArchiveProject(); void slotSetDocumentRenderProfile(const QMap &props); /** @brief Switches between displaying frames or timecode. * @param ix 0 = display timecode, 1 = display frames. */ void slotUpdateTimecodeFormat(int ix); /** @brief Removes the focus of anything. */ void slotRemoveFocus(); void slotCleanProject(); void slotShutdown(); void slotSwitchMonitors(); void slotSwitchMonitorOverlay(QAction *); void slotSwitchDropFrames(bool drop); void slotSetMonitorGamma(int gamma); void slotCheckRenderStatus(); void slotInsertZoneToTree(); /** @brief The monitor informs that it needs (or not) to have frames sent by the renderer. */ void slotMonitorRequestRenderFrame(bool request); /** @brief Update project because the use of proxy clips was enabled / disabled. */ void slotUpdateProxySettings(); /** @brief Disable proxies for this project. */ void slotDisableProxies(); /** @brief Process keyframe data sent from a clip to effect / transition stack. */ void slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes); /** @brief Move playhead to mouse cursor position if defined key is pressed */ void slotAlignPlayheadToMousePos(); void slotThemeChanged(const QString &name); /** @brief Close Kdenlive and try to restart it */ void slotRestart(bool clean = false); void triggerKey(QKeyEvent *ev); /** @brief Update monitor overlay actions on monitor switch */ void slotUpdateMonitorOverlays(int id, int code); /** @brief Update widget style */ void slotChangeStyle(QAction *a); /** @brief Create temporary top track to preview an effect */ void createSplitOverlay(Mlt::Filter *filter); void removeSplitOverlay(); /** @brief Create a generator's setup dialog */ void buildGenerator(QAction *action); void slotCheckTabPosition(); /** @brief Toggle automatic timeline preview on/off */ void slotToggleAutoPreview(bool enable); /** @brief Rebuild/reload timeline toolbar. */ void rebuildTimlineToolBar(); void showTimelineToolbarMenu(const QPoint &pos); /** @brief Open Cached Data management dialog. */ void slotManageCache(); void showMenuBar(bool show); /** @brief Change forced icon theme setting (asks for app restart). */ void forceIconSet(bool force); /** @brief Toggle current project's compositing mode. */ void slotUpdateCompositing(QAction *compose); /** @brief Update compositing action to display current project setting. */ void slotUpdateCompositeAction(int mode); /** @brief Cycle through the different timeline trim modes. */ void slotSwitchTrimMode(); void setTrimMode(const QString &mode); /** @brief Set timeline toolbar icon size. */ void setTimelineToolbarIconSize(QAction *a); void slotEditItemSpeed(); void updateAction(); /** @brief Request adjust of timeline track height */ void resetTimelineTracks(); /** @brief Set keyboard grabbing on current timeline item */ void slotGrabItem(); signals: Q_SCRIPTABLE void abortRenderJob(const QString &url); void configurationChanged(); void GUISetupDone(); void setPreviewProgress(int); void setRenderProgress(int); void displayMessage(const QString &, MessageType, int); /** @brief Project profile changed, update render widget accordingly. */ void updateRenderWidgetProfile(); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId = -1); void adjustAssetPanelRange(int itemId, int in, int out); /** @brief Enable or disable the undo stack. For example undo/redo should not be enabled when dragging a clip in timeline or we risk corruption. */ void enableUndo(bool enable); }; #endif diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index c048d730c..b5f6576ec 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,3478 +1,3476 @@ /*************************************************************************** * 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 #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("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock")) .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "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("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "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("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("itemIds")) .method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks")) .method("suggestCompositionMove", &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance")) // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("requestCompositionInsertion", select_overload, int &, bool)>( // &TimelineModel::requestCompositionInsertion))( // parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo")) .method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","changeDuration")); } 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_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_blockRefresh(false) , m_closing(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_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } void TimelineModel::prepareClose() { requestClearSelection(true); QWriteLocker locker(&m_lock); // Unlock all tracks to allow delting clip from tracks m_closing = true; auto it = m_allTracks.begin(); while (it != m_allTracks.end()) { (*it)->unlock(); ++it; } } 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.cbegin(); 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(isItem(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.cbegin(); 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, int separated) const { if (separated == 1) { return getTrackPosition(trackId) + 1; } if (separated == 2) { // Count audio/video tracks auto it = m_allTracks.cbegin(); int aCount = 0; int vCount = 0; int refPos = 0; bool isVideo = true; while (it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { if ((*it)->getId() == trackId) { refPos = aCount; isVideo = false; } aCount++; } else { // video track if ((*it)->getId() == trackId) { refPos = vCount; } vCount++; } ++it; } return isVideo ? aCount + refPos + 1 : aCount - refPos; } auto it = m_allTracks.cend(); 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.cbegin()) { --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.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return (*it)->getId(); } } return 0; } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return getTrackMltIndex((*it)->getId()); } } return 0; } 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; while (it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { count++; } else { count--; if (count == 0) { return (*it)->getId(); } } ++it; } 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... qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO"; return -1; } int count = 0; while (it != m_allTracks.cbegin()) { if (!(*it)->isAudioTrack()) { count++; } else { count--; if (count == 0) { return (*it)->getId(); } } --it; } if ((*it)->isAudioTrack() && count == 1) { 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::requestFakeClipMove(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 moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<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; // qDebug()<<"MOVING CLIP FROM: "< 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(); bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); TRACE_RES(res); return res; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, 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, moveMirrorTracks, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, 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, true, false, 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 moveMirrorTracks) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, cursorPosition, snapDistance); 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) { TRACE_RES(position); return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = getBestSnapPos(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, moveMirrorTracks, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { TRACE_RES(position); return position; } if (sourceTrackId == -1) { // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away... TRACE_RES(currentPos); return currentPos; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId && sourceTrackId != -1) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { TRACE_RES(position); 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 { TRACE_RES(currentPos); return currentPos; } possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); TRACE_RES(possible ? position : currentPos); 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) - 1 : 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, moveMirrorTracks, true, false, false); if (possible) { TRACE_RES(updatedPos); return updatedPos; } } TRACE_RES(currentPos); return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(compoId, trackId, position, cursorPosition, snapDistance); 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) { TRACE_RES(position); 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 = getBestSnapPos(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) { TRACE_RES(position); 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;*/ TRACE_RES(currentPos); 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) { Fun local_undo = []() { return true; }; Fun 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; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } else if (useTargets && getTrackById_const(trackId)->isLocked()) { // Video target set but locked trackId = m_audioTarget; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } if (trackId == -1) { pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); return false; } 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, true, refreshView, logUndo, logUndo, local_undo, local_redo); int target_track; if (audioDrop) { target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget; } else { target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget; } qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); int mirror = getMirrorTrackId(trackId); if (mirror > -1 && getTrackById_const(mirror)->isLocked()) { mirror = -1; } 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, true, true, true, 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, true, refreshView, logUndo, 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 itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, undo, redo); } if (isClip(itemId)) { return requestClipDeletion(itemId, undo, redo); } if (isComposition(itemId)) { return requestCompositionDeletion(itemId, undo, redo); } Q_ASSERT(false); return false; } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isItem(itemId)); QString actionLabel; if (m_groups->isInGroup(itemId)) { actionLabel = i18n("Remove group"); } else { if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); } else { actionLabel = i18n("Delete Composition"); } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestItemDeletion(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, false, true); 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()) { 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, true); if (!res) { undo(); return false; } else { Fun unplant_op = [this, compositionId]() { unplantComposition(compositionId); return true; }; unplant_op(); PUSH_LAMBDA(unplant_op, redo); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; int new_in = composition->getPosition(); int new_out = new_in + composition->getPlaytime(); Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() { // 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); composition->setCurrentTrackId(trackId, true); replantCompositions(compositionId, false); checkRefresh(new_in, new_out); return true; }; if (operation()) { Fun update_monitor = [this, new_in, new_out]() { checkRefresh(new_in, new_out); return true; }; update_monitor(); PUSH_LAMBDA(update_monitor, operation); 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) { TRACE(clipId, groupId, delta_track, delta_pos, updateView, 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")); } TRACE_RES(res); 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 // 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 itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, moveMirrorTracks); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool moveMirrorTracks, bool allowViewRefresh) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); Q_ASSERT(isItem(itemId)); if (getGroupElements(groupId).count(itemId) == 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; }; std::unordered_set all_clips; std::unordered_set all_compositions; // Separate clips from compositions to sort for (int affectedItemId : all_items) { if (isClip(affectedItemId)) { all_clips.insert(affectedItemId); } else { all_compositions.insert(affectedItemId); } } // Sort clips first std::vector sorted_clips(all_clips.begin(), all_clips.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) { int p1 = m_allClips[clipId1]->getPosition(); int p2 = m_allClips[clipId2]->getPosition(); return delta_pos > 0 ? p2 <= p1 : p1 <= p2; }); // Sort compositions. We need to delete in the move direction from top to bottom std::vector sorted_compositions(all_compositions.begin(), all_compositions.end()); std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) { int p1 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2; }); sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end()); // 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 = [this, finalMove]() { if (finalMove) { updateDuration(); } return true; }; // Check if there is a track move bool updatePositionOnly = false; // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, finalMove, 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); } if (finalMove) { updateDuration(); } return true; }; } std::unordered_map old_track_ids, old_position, old_forced_track; // First, remove clips if (delta_track != 0) { // We delete our clips only if changing 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, true, false); 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; } } } if (!moveMirrorTracks) { if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = 0; } else { audio_delta = 0; } } else { if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } } } // We need to insert depending on the move direction to avoid confusing the view // std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); bool updateThisView = allowViewRefresh; if (delta_track == 0) { // Special case, we are moving on same track, avoid too many calculations for (int item : sorted_clips) { int current_track_id = getItemTrackId(item); int target_position = getItemPosition(item) + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } else { ok = ok && requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } else { // Track changed 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; 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, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } 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); if (!m_groups->isInGroup(clipId)) { TRACE_RES(false); return false; } bool res = requestItemDeletion(clipId, logUndo); 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(); bool isSelection = m_currentSelection == current_group; if (isSelection) { 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) { if (m_groups->getType(current_group) == GroupType::Selection) { Q_ASSERT(isSelection); // in the case of a selection group, we delete the group but don't log it in the undo object Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; m_groups->ungroupItem(one_child, tmp_undo, tmp_redo); } else { 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; } const QVariantList TimelineModel::getGroupData(int itemId) { QWriteLocker locker(&m_lock); if (!m_groups->isInGroup(itemId)) { return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)}; } int groupId = m_groups->getRootId(itemId); QVariantList result; std::unordered_set items = m_groups->getLeaves(groupId); for (int id : items) { result << id << getItemPosition(id) << getItemPlaytime(id); } return result; } void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right) { Q_ASSERT(startPos.size() == endPos.size()); QMap> startData; QMap> endData; while (!startPos.isEmpty()) { int id = startPos.takeFirst().toInt(); int in = startPos.takeFirst().toInt(); int duration = startPos.takeFirst().toInt(); startData.insert(id, {in, duration}); id = endPos.takeFirst().toInt(); in = endPos.takeFirst().toInt(); duration = endPos.takeFirst().toInt(); endData.insert(id, {in, duration}); } QMapIterator> i(startData); QList changedItems; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; while (i.hasNext()) { i.next(); QPair startItemPos = i.value(); QPair endItemPos = endData.value(i.key()); if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) { // Revert individual items to original position requestItemResize(i.key(), startItemPos.second, right, false, 0, true); changedItems << i.key(); } } for (int id : changedItems) { QPair endItemPos = endData.value(id); result = result & requestItemResize(id, endItemPos.second, right, true, undo, redo, false); if (!result) { break; } } if (result) { PUSH_UNDO(undo, redo, i18n("Resize group")); } else { undo(); } } const std::vector TimelineModel::getBoundaries(int itemId) { std::vector boundaries; std::unordered_set items; if (m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); items = m_groups->getLeaves(groupId); } else { items.insert(itemId); } for (int id : items) { if (isClip(id) || isComposition(id)) { int in = getItemPosition(id); int out = in + getItemPlaytime(id); boundaries.push_back(in); boundaries.push_back(out); } } return boundaries; } int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed) { QWriteLocker locker(&m_lock); TRACE(itemId, size, right, true, snapDistance, allowSingleResize); Q_ASSERT(isClip(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); //size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); 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); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } else { all_items.insert(itemId); } 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) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } // First delete clip, then timewarp, resize and reinsert int pos = getItemPosition(id); if (!right) { pos += getItemPlaytime(id) - size; } result = getTrackById(tid)->requestClipDeletion(id, true, true, undo, redo, false, true); result = result && requestClipTimeWarp(id, speed, false, undo, redo); result = result && requestItemResize(id, size, true, true, undo, redo); result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, true, undo, redo); if (!result) { break; } } if (!result) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result) { PUSH_UNDO(undo, redo, i18n("Resize clip speed")); } int res = result ? size : -1; TRACE_RES(res); return res; } int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance) { if (snapDistance > 0 && getItemTrackId(itemId) != -1) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; if (right && size > out - in && isClip(itemId)) { int targetPos = in + size - 1; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; } } else if (!right && size > (out - in) && isClip(itemId)) { int targetPos = out - size; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = out - getTrackById_const(trackId)->getBlankStart(in - 1); } } int timelinePos = pCore->getTimelinePosition(); m_snaps->addPoint(timelinePos); int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); m_snaps->removePoint(timelinePos); 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; } } } return size; } 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(isItem(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); 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); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } all_items.insert(itemId); for (int id : items) { if (id == itemId) { continue; } int start = getItemPosition(id); int end = start + 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; int finalPos = right ? in + size : out - size; int finalSize; int resizedCount = 0; for (int id : all_items) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } if (right) { finalSize = finalPos - getItemPosition(id); } else { finalSize = getItemPosition(id) + getItemPlaytime(id) - finalPos; } result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo); resizedCount++; } if (!result || resizedCount == 0) { 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; }; 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) { 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); if (type == GroupType::Selection || type == GroupType::Leaf) { // Selections shouldn't be done here. Call requestSetSelection instead TRACE_RES(-1); return -1; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; int result = requestClipsGroup(ids, undo, redo, type); if (result > -1 && logUndo) { 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); if (type != GroupType::Selection) { requestClearSelection(); } int clipsCount = 0; QList tracks; for (int id : ids) { if (isClip(id)) { int trackId = getClipTrackId(id); if (trackId == -1) { return -1; } tracks << trackId; clipsCount++; } 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; } if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) { // Check if we are grouping an AVSplit std::unordered_set::const_iterator it = ids.begin(); int firstId = *it; std::advance(it, 1); int secondId = *it; bool isAVGroup = false; if (getClipBinId(firstId) == getClipBinId(secondId)) { if (getClipState(firstId) == PlaylistState::AudioOnly) { if (getClipState(secondId) == PlaylistState::VideoOnly) { isAVGroup = true; } } else if (getClipState(secondId) == PlaylistState::AudioOnly) { isAVGroup = true; } } if (isAVGroup) { type = GroupType::AVSplit; } } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type != GroupType::Selection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } 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; requestClearSelection(); std::unordered_set roots; std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); for (int root : roots) { if (isGroup(root)) { 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; 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); bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection; if (!isSelection) { requestClearSelection(); } bool res = m_groups->ungroupItem(itemId, undo, redo); if (res && !isSelection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return res; } 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, true); 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); if (updateView) { _resetView(); } 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, true); if (updateView) { _resetView(); } 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)); if (m_allTracks.size() < 2) { pCore->displayMessage(i18n("Cannot delete last track in timeline"), InformationMessage, 500); return false; } 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); _resetView(); 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) { // 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; int cache = QThread::idealThreadCount() + (m_allTracks.size() + 1) * 2; mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache)); } 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(); } int cache = QThread::idealThreadCount() + (m_allTracks.size() + 1) * 2; mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache)); return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); requestClearSelection(true); 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::isItem(int id) const { return isClip(id) || isComposition(id); } 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() { if (m_closing) { return; } 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("out", 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::getBestSnapPos(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::getNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::getPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { TRACE(pos); return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { TRACE(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); // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo); 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")); } // TRACE_RES(result); 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 requestClearSelection(true); 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, false); } 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 a_track, and increasing 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) { if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) { return a.first < b.first; } return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack(); }); // 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()); Mlt::Transition &transition = *m_allCompositions[compo.second].get(); int ret = field->plant_transition(transition, 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; 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, false, false); } 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, bool changeDuration, 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, false, false); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, changeDuration, 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, bool changeDuration) { QWriteLocker locker(&m_lock); TRACE(clipId, 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, changeDuration, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, changeDuration, undo, redo); } if (!result) { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); TRACE_RES(false); return false; } } else { // If clip is not inserted on a track, we just change the producer result = m_allClips[clipId]->useTimewarpProducer(speed, changeDuration, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); } TRACE_RES(result); return result; } 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.cbegin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.cend()) { 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.cbegin()) { --it; if ((*it)->isAudioTrack() == audioWanted) { return (*it)->getId(); } } return trackId; } 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.cend()) { ++it; if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.cend() ? trackId : (*it)->getId(); } bool TimelineModel::requestClearSelection(bool onDeletion) { QWriteLocker locker(&m_lock); TRACE(); if (m_currentSelection == -1) { TRACE_RES(true); return true; } if (isGroup(m_currentSelection)) { // Reset offset display on clips std::unordered_set items = m_groups->getLeaves(m_currentSelection); for (auto &id : items) { if (isGroup(id)) { std::unordered_set children = m_groups->getLeaves(id); - for (int c : children) { - items.insert(c); - } + items.insert(children.begin(), children.end()); } else if (isClip(id)) { m_allClips[id]->clearOffset(); m_allClips[id]->setGrab(false); m_allClips[id]->setSelected(false); } else if (isComposition(id)) { m_allCompositions[id]->setGrab(false); m_allCompositions[id]->setSelected(false); } if (m_groups->getType(m_currentSelection) == GroupType::Selection) { m_groups->destructGroupItem(m_currentSelection); } } } else { if (isClip(m_currentSelection)) { m_allClips[m_currentSelection]->setGrab(false); m_allClips[m_currentSelection]->setSelected(false); } else if (isComposition(m_currentSelection)) { m_allCompositions[m_currentSelection]->setGrab(false); m_allCompositions[m_currentSelection]->setSelected(false); } Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection)); } m_currentSelection = -1; emit selectionChanged(); TRACE_RES(true); return true; } 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 all_items = {itemId}; int parentGroup = m_groups->getDirectAncestor(itemId); if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) { all_items = m_groups->getLeaves(parentGroup); } std::unordered_set selection = getCurrentSelection(); for (int current_itemId : all_items) { if (selection.count(current_itemId) > 0) { selection.erase(current_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()); setSelected(m_currentSelection, true); } else { Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (ids.size() == 2) { // Check if we selected 2 clips from the same master QList pairIds; for (auto &id : roots) { if (isClip(id)) { pairIds << id; } } if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) { // Check if they have same bin id // Both clips have same bin ID, display offset int pos1 = getClipPosition(pairIds.at(0)); int pos2 = getClipPosition(pairIds.at(1)); if (pos2 > pos1) { int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0))); if (offset != 0) { m_allClips[pairIds.at(1)]->setOffset(offset); m_allClips[pairIds.at(0)]->setOffset(-offset); } } else { int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1))); if (offset != 0) { m_allClips[pairIds.at(0)]->setOffset(offset); m_allClips[pairIds.at(1)]->setOffset(-offset); } } } } result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0; Q_ASSERT(m_currentSelection >= 0); } emit selectionChanged(); return result; } void TimelineModel::setSelected(int itemId, bool sel) { if (isClip(itemId)) { m_allClips[itemId]->setSelected(sel); } else if (isComposition(itemId)) { m_allCompositions[itemId]->setSelected(sel); } else if (isGroup(itemId)) { auto leaves = m_groups->getLeaves(itemId); for (auto &id : leaves) { setSelected(id, true); } } } bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); 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; } void TimelineModel::setTrackLockedState(int trackId, bool lock) { QWriteLocker locker(&m_lock); TRACE(trackId, lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; Fun lock_lambda = [this, trackId]() { getTrackById(trackId)->lock(); return true; }; Fun unlock_lambda = [this, trackId]() { getTrackById(trackId)->unlock(); return true; }; if (lock) { if (lock_lambda()) { UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Lock track")); } } else { if (unlock_lambda()) { UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Unlock track")); } } } std::unordered_set TimelineModel::getAllTracksIds() const { READ_LOCK(); std::unordered_set result; std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; }); return result; } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index afc82ce75..474a5de6c 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,2501 +1,2517 @@ /*************************************************************************** * 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/clipcreator.hpp" #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 "mainwindow.h" #include "monitor/monitormanager.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 #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); connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording); } TimelineController::~TimelineController() { prepareClose(); } void TimelineController::prepareClose() { // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate 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); 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::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(); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor 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 *(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_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(); } } int TimelineController::selectedTrack() const { 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}); } // 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 (!select) { m_model->requestRemoveFromSelection(currentClip); } else { m_model->requestAddToSelection(currentClip, !addToCurrent); } } QList TimelineController::selection() const { if (!m_root) return QList(); std::unordered_set sel = m_model->getCurrentSelection(); QList items; for (int id : sel) { items << id; } return items; } void TimelineController::selectItems(const QList &ids) { std::unordered_set ids_s(ids.begin(), ids.end()); m_model->requestSetSelection(ids_s); } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } 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; } else { // Check if we have a composition at beginning std::unordered_set existing = m_model->getTrackById_const(tid)->getCompositionsInRange(minimum, minimum + offset); if (existing.size() > 0) { position += offset; } } position = qMin(minimum + clip_duration - 1, position); int duration = qMin(pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()), m_model->getTrackById_const(tid)->suggestCompositionLength(position)); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = offset > clip_duration / 2; 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 = position > minimum; } } } int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (duration2 > 0) { duration = (duration > 0) ? qMin(duration, duration2) : duration2; } } if (duration < 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); } else if (duration <= 1) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (minimum + clip_duration - position < 3) { position = minimum + clip_duration - 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() { auto sel = m_model->getCurrentSelection(); if (sel.empty()) { return; } // only need to delete the first item, the others will be deleted in cascade m_model->requestItemDeletion(*sel.begin()); } int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) { auto sel = m_model->getCurrentSelection(); if (sel.empty() || sel.size() > 2) { return -1; } int itemId = *(sel.begin()); if (sel.size() == 2) { int parentGroup = m_model->m_groups->getRootId(itemId); if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { return -1; } } if (!restrictToCurrentPos) { if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { return itemId; } } if (m_model->isClip(itemId)) { int position = timelinePosition(); int start = m_model->getClipPosition(itemId); int end = start + m_model->getClipPlaytime(itemId); if (position >= start && position <= end) { return itemId; } } return -1; } void TimelineController::copyItem() { 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); m_model->requestSetSelection(selectedIds); } bool TimelineController::pasteItem(int position, int tid) { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (tid == -1) { tid = getMouseTrack(); } if (position == -1) { position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } 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; bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); if (addAVTrack) { int newTid2; int mirrorPos = 0; int mirrorId = m_model->getMirrorAudioTrackId(newTid); if (mirrorId > -1) { mirrorPos = m_model->getTrackMltIndex(mirrorId); } m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true); } m_model->buildTrackCompositing(true); if (audioRecTrack) { m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1")); } } } 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(); if (m_activeTrack == selectedTrackIx) { // Make sure we don't keep an index on a deleted track m_activeTrack = -1; } if (m_model->m_audioTarget == selectedTrackIx) { setAudioTarget(-1); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } m_model->requestTrackDeletion(selectedTrackIx); m_model->buildTrackCompositing(true); if (m_activeTrack == -1) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::showConfig(int page, int tab) { pCore->showConfigDialog(page, tab); } void TimelineController::gotoNextSnap() { int nextSnap = m_model->getNextSnapPos(timelinePosition()); if (nextSnap > timelinePosition()) { setPosition(nextSnap); } } void TimelineController::gotoPreviousSnap() { if (timelinePosition() > 0) { setPosition(m_model->getPreviousSnapPos(timelinePosition())); } } void TimelineController::groupSelection() { const auto selection = m_model->getCurrentSelection(); if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } m_model->requestClearSelection(); m_model->requestClipsGroup(selection); m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { auto ids = m_model->getCurrentSelection(); // ask to unselect if needed m_model->requestClearSelection(); if (cid > -1) { ids.insert(cid); } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } } bool TimelineController::dragOperationRunning() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toBool(); } void TimelineController::setInPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = timelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; 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); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); if (maximumSpace < INT_MAX) { cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); } } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start != cursorPos) { int size = start + m_model->getItemPlaytime(cid) - cursorPos; m_model->requestItemResize(cid, size, false, true, 0, false); } } } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = timelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; 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); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos); cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1)); } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start + m_model->getItemPlaytime(cid) != cursorPos) { int size = cursorPos - start; m_model->requestItemResize(cid, size, true, true, 0, false); } } } } } void TimelineController::editMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); if (clip->getMarkerModel()->hasMarker(position)) { GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } else { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); } } void TimelineController::addMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); } void TimelineController::addQuickMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } void TimelineController::deleteMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->removeMarker(pos); } void TimelineController::deleteAllMarkers(int cid) { Q_ASSERT(m_model->isClip(cid)); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); clip->getMarkerModel()->removeAllMarkers(); } 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(); const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { QList effectSelection; for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; } } 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::adjustAllTrackHeight(int trackId, int height) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); } ++it; } int tracksCount = m_model->getTracksCount(); 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}); } 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 &tracks, int startFrame, int endFrame, bool addToSelect) { std::unordered_set itemsToSelect; if (addToSelect) { itemsToSelect = m_model->getCurrentSelection(); } for (int i = 0; i < tracks.count(); i++) { if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { continue; } auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } 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; 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) { 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(bool resetZones) { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(resetZones); } } 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(true); 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; } bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); if (!res) { pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500); } } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || !m_model->isItem(cid) || 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 (clipId == -1) { clipId = getMainSelectedItem(false, true); } if (clipId == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); 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))); // 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))); 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, true); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode)); QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 0; 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("aligned", 0); 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; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != audioTarget() && target_track != videoTarget() && !m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { ++it; continue; } tracks << target_track; ++it; } 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, {track}, zone, false); } bool TimelineController::insertClipZone(const QString &binId, int tid, int position) { QStringList binIdData = binId.split(QLatin1Char('/')); int in = 0; int out = -1; if (binIdData.size() >= 3) { in = binIdData.at(1).toInt(); out = binIdData.at(2).toInt(); } QString bid = binIdData.first(); // 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); } int aTrack = -1; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = clip->frameDuration() - 1; } if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { aTrack = tid; } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { aTrack = tid; vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1; } else { vTrack = tid; aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1; } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, position, QPoint(in, out), m_model->m_editMode == TimelineMode::OverwriteEdit, 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) { + if (clipId == -1) { + clipId = getMainSelectedItem(false, false); + } TimelineFunctions::switchEnableState(m_model, clipId); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { int track = m_model->getClipTrackId(clipId); int compoId = -1; if (assetId.isEmpty()) { QStringList compositions = KdenliveSettings::favorite_transitions(); if (compositions.isEmpty()) { pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500); return; } compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); } else { compoId = insertNewComposition(track, clipId, offset, assetId, true); } if (compoId > 0) { m_model->requestSetSelection({compoId}); } } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { qDebug() << "/// ADDING ASSET: " << assetId; m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { 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, 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 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, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, 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::switchTrackActive(int trackId) { if (trackId == -1) { trackId = m_activeTrack; } bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); m_model->setTrackLockedState(m_activeTrack, !locked); } else { // Invert track lock const auto ids = m_model->getAllTracksIds(); // count the number of tracks to be locked int toBeLockedCount = std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); }); bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount(); for (const int id : ids) { // leave active track unlocked if (leaveOneUnlocked && id == m_activeTrack) { continue; } bool isLocked = m_model->getTrackById_const(id)->isLocked(); m_model->setTrackLockedState(id, !isLocked); } } } 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}); } void TimelineController::selectAll() { std::unordered_set ids; for (auto clp : m_model->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { std::unordered_set ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { std::unordered_set targetIds; if (targetId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } for (int s : sel) { if (m_model->isGroup(s)) { std::unordered_set sub = m_model->m_groups->getLeaves(s); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(s)) { targetIds.insert(s); } } } else { if (m_model->m_groups->isInGroup(targetId)) { targetId = m_model->m_groups->getRootId(targetId); } if (m_model->isGroup(targetId)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetId); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(targetId)) { targetIds.insert(targetId); } } if (targetIds.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (txt.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); if (clips.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); for (int i = 1; i < clips.size(); i++) { QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); QDomNodeList subs = subeffects.childNodes(); while (!subs.isEmpty()) { subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); effects.appendChild(subs.at(0)); } } bool result = true; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); result = result && destStack->fromXml(effects, undo, redo); if (!result) { break; } } 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) { if (id == -1) { id = getMainSelectedItem(false, true); } if (id == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } 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, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, 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, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, 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_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); // nothing selected emit showItemEffectStack(QString(), nullptr, QSize(), false); return; } std::shared_ptr clip(nullptr); int item = *m_model->getCurrentSelection().begin(); if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) { showAsset(item); } 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 = isInSelection(item); } else if (actionData == QLatin1Char('U')) { 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('W')) { + enableAction = clip != nullptr; + if (enableAction) { + act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip")); + } } 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_model->getCurrentSelection().empty()) { // TODO: error displayMessage return; } - int id = *m_model->getCurrentSelection().begin(); - while (m_model->isGroup(id)) { - id = *m_model->m_groups->getLeaves(id).begin(); + std::unordered_set ids = m_model->getCurrentSelection(); + std::unordered_set items_list; + for (int i : ids) { + if (m_model->isGroup(i)) { + std::unordered_set children = m_model->m_groups->getLeaves(i); + items_list.insert(children.begin(), children.end()); + } else { + items_list.insert(i); + } } - if (m_model->isClip(id)) { - std::shared_ptr clip = m_model->getClipPtr(id); - clip->setGrab(!clip->isGrabbed()); - } else if (m_model->isComposition(id)) { - std::shared_ptr clip = m_model->getCompositionPtr(id); - clip->setGrab(!clip->isGrabbed()); + for (int id : items_list) { + if (m_model->isClip(id)) { + std::shared_ptr clip = m_model->getClipPtr(id); + clip->setGrab(!clip->isGrabbed()); + } else if (m_model->isComposition(id)) { + std::shared_ptr clip = m_model->getCompositionPtr(id); + clip->setGrab(!clip->isGrabbed()); + } } } 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, false, false); } 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::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, false); } 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, false, false); } 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::requestInsertSpace(m_model, QPoint(min, max), undo, redo, false); } 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, true, updateView, finalMove, 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->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_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); } } bool TimelineController::darkBackground() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5; } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::LinkText).color(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::lockedColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NegativeText).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::selectionColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::NeutralText).color(); } void TimelineController::switchRecording(int trackId) { if (!pCore->isMediaCapturing()) { qDebug() << "start recording" << trackId; if (!m_model->isTrack(trackId)) { qDebug() << "ERROR: Starting to capture on invalid track " << trackId; } if (m_model->getTrackById_const(trackId)->isLocked()) { pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); return; } m_recordStart.first = timelinePosition(); m_recordTrack = trackId; int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); if (maximumSpace == INT_MAX) { m_recordStart.second = 0; } else { m_recordStart.second = maximumSpace - m_recordStart.first; if (m_recordStart.second < 8) { pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " "choose a different track"), ErrorMessage, 500); return; } } pCore->monitorManager()->slotSwitchMonitors(false); pCore->startMediaCapture(true, false); pCore->monitorManager()->slotPlay(); } else { pCore->stopMediaCapture(true, false); pCore->monitorManager()->slotPause(); } } void TimelineController::finishRecording(const QString &recordedFile) { if (recordedFile.isEmpty()) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::function callBack = [this](const QString &binId) { int id = -1; if (m_recordTrack == -1) { return; } qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; bool res = false; if (m_recordStart.second > 0) { // Limited space on track std::shared_ptr clip = pCore->bin()->getBinClip(binId); if (!clip) { return; } int out = qMin((int)clip->frameDuration() - 1, m_recordStart.second - 1); QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out); res = m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); } else { res = m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); } }; QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack); if (binId != QStringLiteral("-1")) { pCore->pushUndo(undo, redo, i18n("Record audio")); } } diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index c565ba438..b3e0d5578 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,551 +1,551 @@ /*************************************************************************** * 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 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 snap READ snap NOTIFY snapChanged) 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 lockedColor READ lockedColor NOTIFY colorsChanged) Q_PROPERTY(QColor selectionColor READ selectionColor 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); /** @brief Edit an item's in/out points with a dialog */ Q_INVOKABLE void editItemDuration(int itemId = -1); /** @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 request a selection with a list of ids*/ Q_INVOKABLE void selectItems(const QList &ids); /* @brief Returns true is item is selected as well as other items */ Q_INVOKABLE bool isInSelection(int itemId); /* @brief Show/hide audio record controls on a track */ Q_INVOKABLE void switchRecording(int trackId); /* @brief Add recorded file to timeline */ void finishRecording(const QString &recordedFile); /* @brief Open Kdenlive's config diablog on a defined page and tab */ Q_INVOKABLE void showConfig(int page, int tab); /* @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 lockedColor() const; Q_INVOKABLE QColor selectionColor() 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); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(int position = -1, int tid = -1); /* @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 Returns id of the timeline selcted clip if there is only 1 clip selected * or an AVSplit group. If allowComposition is true, returns composition id if * only 1 is selected, otherwise returns -1. If restrictToCurrentPos is true, it will * only return the id if timeline cursor is inside item */ int getMainSelectedItem(bool restrictToCurrentPos = true, bool allowComposition = false); /* @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(int cid, int position = -1); /* @brief Ask for marker add dialog */ Q_INVOKABLE void addMarker(int cid, int position = -1); /* @brief Ask for quick marker add (without dialog) */ Q_INVOKABLE void addQuickMarker(int cid, int position = -1); /* @brief Ask for marker delete */ Q_INVOKABLE void deleteMarker(int cid, int position = -1); /* @brief Ask for all markers delete */ Q_INVOKABLE void deleteAllMarkers(int cid); /* @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); /* @brief Adjust height of all simlar (audio or video) tracks */ Q_INVOKABLE void adjustAllTrackHeight(int trackId, int height); 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 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 switchEnableState(int clipId = -1); 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()); Q_INVOKABLE void switchTrackActive(int trackId = -1); 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 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(bool resetZones); 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 */ Q_INVOKABLE bool insertClipZone(const QString &binId, int tid, int pos); 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 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(); /** @brief Returns true if a drag operation is currently running in timeline */ bool dragOperationRunning(); /** @brief Disconnect some stuff before closing project */ void prepareClose(); public slots: 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 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; int m_position; int m_seekPosition; int m_audioTarget; int m_videoTarget; int m_activeTrack; int m_audioRef; QPair m_recordStart; int m_recordTrack; QPoint m_zone; double m_scale; static int m_duration; PreviewManager *m_timelinePreview; QAction *m_disablePreview; std::shared_ptr m_audioCorrelator; QMutex m_metaMutex; int getCurrentItem(); void initializePreview(); bool darkBackground() 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); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(); Q_INVOKABLE void ungrabHack(); }; #endif