diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index 00169e9d8..e409e44f0 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2330 +1,2331 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "monitor.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "jobs/jobmanager.h" #include "jobs/cutclipjob.h" #include "scopes/monitoraudiolevel.h" #include "timeline2/model/snapmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/thumbnailcache.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { auto *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_snaps(new SnapModel()) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_contextMenu(nullptr) , m_markerMenu(nullptr) , m_audioChannels(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_offset(id == Kdenlive::ProjectMonitor ? TimelineModel::seekDuration : 0) , m_lastMonitorSceneType(MonitorSceneDefault) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; m_glWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::requestSeek, this, &Monitor::processSeek, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::positionChanged, this, &Monitor::slotSeekPosition); connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); connect(m_qmlManager, &QmlManager::activateTrack, this, &Monitor::activateTrack); auto *monitorEventEater = new QuickMonitorEventEater(this); m_videoWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek); connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); QComboBox *scalingAction = new QComboBox(this); scalingAction->setToolTip(i18n("Preview resolution - lower resolution means faster preview")); // Combobox padding is bad, so manually add a space before text scalingAction->addItems({QStringLiteral(" ") + i18n("1:1"),QStringLiteral(" ") + i18n("720p"),QStringLiteral(" ") + i18n("540p"),QStringLiteral(" ") + i18n("360p"),QStringLiteral(" ") + i18n("270p")}); connect(scalingAction, QOverload::of(&QComboBox::activated), [this] (int index) { switch (index) { case 1: KdenliveSettings::setPreviewScaling(2); break; case 2: KdenliveSettings::setPreviewScaling(4); break; case 3: KdenliveSettings::setPreviewScaling(8); break; case 4: KdenliveSettings::setPreviewScaling(16); break; default: KdenliveSettings::setPreviewScaling(0); } m_monitorManager->scalingChanged(); m_monitorManager->updatePreviewScaling(); }); connect(manager, &MonitorManager::updatePreviewScaling, [this, scalingAction]() { m_glMonitor->updateScaling(); switch (KdenliveSettings::previewScaling()) { case 2: scalingAction->setCurrentIndex(1); break; case 4: scalingAction->setCurrentIndex(2); break; case 8: scalingAction->setCurrentIndex(3); break; case 16: scalingAction->setCurrentIndex(4); break; default: scalingAction->setCurrentIndex(0); break; } refreshMonitorIfActive(); }); scalingAction->setFrame(false); scalingAction->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_toolbar->addWidget(scalingAction); m_toolbar->addSeparator(); m_speedLabel = new QLabel(this); m_speedLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Button); QColor bg = scheme.background(KColorScheme::LinkBackground).color(); bg = scheme.background(KColorScheme::PositiveBackground).color(); m_speedLabel->setStyleSheet(QString("padding-left: %4; padding-right: %4;background-color: rgb(%1,%2,%3);").arg(bg.red()).arg(bg.green()).arg(bg.blue()).arg(m_speedLabel->sizeHint().height()/4)); m_toolbar->addWidget(m_speedLabel); m_speedLabel->setFixedWidth(0); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree"))); m_toolbar->setToolTip(i18n("Insert Zone to Project Bin")); m_toolbar->addSeparator(); m_streamsButton = new QToolButton(this); m_streamsButton->setPopupMode(QToolButton::InstantPopup); m_streamsButton->setIcon(QIcon::fromTheme(QStringLiteral("speaker"))); m_streamAction = m_toolbar->addWidget(m_streamsButton); m_audioChannels = new QMenu(this); m_streamsButton->setMenu(m_audioChannels); m_streamAction->setVisible(false); connect(m_audioChannels, &QMenu::triggered, [this] () { m_audioChannels->show(); QList actions = m_audioChannels->actions(); QMap enabledStreams; for (const auto act : actions) { if (act->isChecked()) { // Audio stream is selected enabledStreams.insert(act->data().toInt(), act->text().remove(QLatin1Char('&'))); } } if (!enabledStreams.isEmpty()) { // Only 1 stream wanted, easy - m_glMonitor->getControllerProxy()->setAudioStream(enabledStreams.first()); QMap props; props.insert(QStringLiteral("audio_index"), QString::number(enabledStreams.firstKey())); - if (enabledStreams.count() > 1) { - // Mix audio channels - - } QList streams = enabledStreams.keys(); QStringList astreams; for (const int st : streams) { astreams << QString::number(st); } props.insert(QStringLiteral("kdenlive:active_streams"), astreams.join(QLatin1Char(';'))); m_controller->setProperties(props, true); } else { // No active stream - m_glMonitor->getControllerProxy()->setAudioStream(QString()); QMap props; props.insert(QStringLiteral("audio_index"), QStringLiteral("-1")); props.insert(QStringLiteral("kdenlive:active_streams"), QStringLiteral("-1")); m_controller->setProperties(props, true); } }); } else if (id == Kdenlive::ProjectMonitor) { connect(m_glMonitor, &GLWidget::paused, m_monitorManager, &MonitorManager::cleanMixer); } if (id != Kdenlive::DvdMonitor) { QAction *markIn = new QAction(QIcon::fromTheme(QStringLiteral("zone-in")), i18n("Set Zone In"), this); QAction *markOut = new QAction(QIcon::fromTheme(QStringLiteral("zone-out")), i18n("Set Zone Out"), this); m_toolbar->addAction(markIn); m_toolbar->addAction(markOut); connect(markIn, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_in"))->trigger(); }); connect(markOut, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_out"))->trigger(); }); } // Per monitor rewind action QAction *rewind = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-backward")), i18n("Rewind"), this); m_toolbar->addAction(rewind); connect(rewind, &QAction::triggered, this, &Monitor::slotRewind); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); connect(m_playMenu, &QMenu::aboutToShow, this, &Monitor::slotActivateMonitor); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); // Per monitor forward action QAction *forward = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-forward")), i18n("Forward"), this); m_toolbar->addAction(forward); connect(forward, &QAction::triggered, [this]() { Monitor::slotForward(); }); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(KdenliveSettings::volume()); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(i18n("Volume"), this); menu->setIcon(QIcon::fromTheme(QStringLiteral("audio-volume-medium"))); menu->addAction(widgetslider); m_configMenu->addMenu(menu); /*QIcon icon; if (KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon);*/ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); if (id == Kdenlive::ProjectMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::zoneUpdated); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZoneWithUndo, this, &Monitor::zoneUpdatedWithUndo); } else if (id == Kdenlive::ClipMonitor) { connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone); } connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame); m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(pCore->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); m_toolbar->addSeparator(); QMargins mrg = m_toolbar->contentsMargins(); m_audioMeterWidget = new MonitorAudioLevel(m_toolbar->height() - mrg.top() - mrg.bottom(), this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Monitor dropped fps timer m_droppedTimer.setInterval(1000); m_droppedTimer.setSingleShot(false); connect(&m_droppedTimer, &QTimer::timeout, this, &Monitor::checkDrops); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; Kdenlive::MonitorId id = m_monitorManager->activeMonitor()->id(); m_monitorManager->activateMonitor(m_id); refreshMonitorIfActive(true); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } m_monitorManager->activateMonitor(id); } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = QIcon::fromTheme(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->recAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->objectName() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { //m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("insert_project_tree"))); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("monitor_multitrack"))); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::primaryScreen()->geometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = pCore->getCurrentProfile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(pCore->getCurrentFps()); QString position = pCore->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(pCore->getCurrentFps()); QString position = pCore->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->getControllerProxy()->setPosition(getSnapForPos(true).frames(pCore->getCurrentFps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->getControllerProxy()->setPosition(getSnapForPos(false).frames(pCore->getCurrentFps())); } } int Monitor::position() { return m_glMonitor->getControllerProxy()->getPosition(); } GenTime Monitor::getSnapForPos(bool previous) { int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return {frame, pCore->getCurrentFps()}; } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y(), false); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); checkOverlay(); } void Monitor::slotSetZoneEnd() { m_glMonitor->getControllerProxy()->setZoneOut(m_glMonitor->getCurrentPos() + 1); checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { if (m_markerMenu) { // Fill guide menu m_markerMenu->clear(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { QList markersList = model->getAllMarkers(); for (CommentedTime mkr : markersList) { QAction *a = m_markerMenu->addAction(mkr.comment()); a->setData(mkr.time().frames(pCore->getCurrentFps())); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum()); float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum()); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum()))); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep()); m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical)); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } bool Monitor::monitorIsFullScreen() const { return m_glWidget->isFullScreen(); } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? if (!m_glWidget->isFullScreen() && !minimizeOnly) { // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget) if (qApp->screens().count() > 1) { for (auto screen : qApp->screens()) { QRect screenRect = screen->availableGeometry(); if (!screenRect.contains(pCore->window()->geometry().center())) { m_glWidget->setParent(nullptr); m_glWidget->move(this->parentWidget()->mapFromGlobal(screenRect.center())); m_glWidget->setGeometry(screenRect); break; } } } else { m_glWidget->setParent(nullptr); } m_glWidget->showFullScreen(); m_videoWidget->setFocus(); } else { m_glWidget->showNormal(); auto *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; QByteArray prodData; QPoint p = m_glMonitor->getControllerProxy()->zone(); if (p.x() == -1 || p.y() == -1) { prodData = m_controller->AbstractProjectItem::clipId().toUtf8(); } else { QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); list.append(QString::number(p.x())); list.append(QString::number(p.y() - 1)); prodData.append(list.join(QLatin1Char('/')).toUtf8()); } mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->exec(Qt::MoveAction); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray clipData; clipData.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), clipData); drag->setMimeData(mimeData); drag->exec(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_glWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, uint modifiers) { if ((modifiers & Qt::ShiftModifier) != 0u) { int delta = qRound(pCore->getCurrentFps()); if (eventDelta > 0) { delta = 0 - delta; } delta = qMax(0, m_glMonitor->getCurrentPos() - delta); m_glMonitor->getControllerProxy()->setPosition(qMin(delta, m_glMonitor->duration() - 1)); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToPreviousSnap(); } else { emit seekToNextSnap(); } } else { if (eventDelta >= 0) { slotRewindOneFrame(); } else { slotForwardOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } GenTime inPoint(getZoneStart(), pCore->getCurrentFps()); GenTime outPoint(getZoneEnd(), pCore->getCurrentFps()); pCore->jobManager()->startJob({m_controller->clipId()}, -1, QString(), inPoint, outPoint); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled")) .completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); fileWidget->setSelectedUrl(relativeUrl); KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QString folderInfo = pCore->bin()->getCurrentFolder(); pCore->bin()->droppedUrls(QList {QUrl::fromLocalFile(selectedFile)}, folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(pos); m_monitorManager->cleanMixer(); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } std::shared_ptr model(nullptr); if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { model = m_controller->getMarkerModel(); } } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, pCore->getCurrentFps()), &found); if (found) { overlayText = marker.comment(); } } m_glMonitor->getControllerProxy()->setMarkerComment(overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getControllerProxy()->zoneOut() - 1); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed > -1) { m_glMonitor->purgeCache(); speed = -1; resetSpeedInfo(); } else { m_speedIndex++; if (m_speedIndex > 4) { m_speedIndex = 0; } speed = -MonitorManager::speedArray[m_speedIndex]; m_speedLabel->setFixedWidth(QWIDGETSIZE_MAX); m_speedLabel->setText(QString("x%1").arg(speed)); } } m_playAction->setActive(true); m_glMonitor->switchPlay(true, speed); } void Monitor::slotForward(double speed, bool allowNormalPlay) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed < 1) { if (allowNormalPlay) { m_glMonitor->purgeCache(); resetSpeedInfo(); m_playAction->setActive(true); m_glMonitor->switchPlay(true, 1); return; } else { m_speedIndex = 0; } } else { m_speedIndex++; } if (m_speedIndex > 4) { m_speedIndex = 0; } speed = MonitorManager::speedArray[m_speedIndex]; m_speedLabel->setFixedWidth(QWIDGETSIZE_MAX); m_speedLabel->setText(QString("x%1").arg(speed)); } m_playAction->setActive(true); m_glMonitor->switchPlay(true, speed); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(qMax(0, m_glMonitor->getCurrentPos() - diff)); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->getControllerProxy()->setPosition(qMin(m_glMonitor->duration() - 1, m_glMonitor->getCurrentPos() + diff)); } else { m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getCurrentPos() + diff); } } void Monitor::adjustRulerSize(int length, const std::shared_ptr &markerModel) { if (m_controller != nullptr) { m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } m_timePos->setRange(0, length); if (markerModel) { connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { if (slotActivateMonitor()) { start(); } } } void Monitor::forceMonitorRefresh() { slotActivateMonitor(); m_glMonitor->refresh(); } void Monitor::refreshMonitorIfActive(bool directUpdate) { if (isActive()) { if (directUpdate) { m_glMonitor->refresh(); } else { m_glMonitor->requestRefresh(); } } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); resetSpeedInfo(); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); resetSpeedInfo(); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); bool showDropped = false; if (m_id == Kdenlive::ClipMonitor) { showDropped = KdenliveSettings::displayClipMonitorInfo() & 0x20; } else if (m_id == Kdenlive::ProjectMonitor) { showDropped = KdenliveSettings::displayProjectMonitorInfo() & 0x20; } if (showDropped) { m_glMonitor->resetDrops(); m_droppedTimer.start(); } else { m_droppedTimer.stop(); } resetSpeedInfo(); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::resetPlayOrLoopZone(const QString &binId) { if (activeClipId() == binId) { m_glMonitor->resetZoneMode(); } } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } void Monitor::updateClipProducer(const std::shared_ptr &prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { Q_UNUSED(playlist) // TODO // Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); // m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } void Monitor::slotOpenClip(const std::shared_ptr &controller, int in, int out) { if (m_controller) { m_glMonitor->resetZoneMode(); disconnect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; m_glMonitor->getControllerProxy()->setAudioStream(QString()); m_snaps.reset(new SnapModel()); m_glMonitor->getControllerProxy()->resetZone(); if (controller) { m_audioChannels->clear(); if (m_controller->audioInfo()) { QMap audioStreamsInfo = m_controller->audioStreams(); if (audioStreamsInfo.size() > 1) { // Multi stream clip QMapIterator i(audioStreamsInfo); - QList activeStreams = m_controller->activeStreams().keys(); + QMap activeStreams = m_controller->activeStreams(); + if (activeStreams.size() > 1) { + m_glMonitor->getControllerProxy()->setAudioStream(i18n("%1 audio streams", activeStreams.size())); + // TODO: Mix audio channels + } else { + m_glMonitor->getControllerProxy()->setAudioStream(m_controller->activeStreams().first()); + } QAction *ac; while (i.hasNext()) { i.next(); ac = m_audioChannels->addAction(i.value()); ac->setData(i.key()); ac->setCheckable(true); if (activeStreams.contains(i.key())) { ac->setChecked(true); - m_glMonitor->getControllerProxy()->setAudioStream(ac->text().remove(QLatin1Char('&'))); } } ac = m_audioChannels->addAction(i18n("Merge all streams")); ac->setData(INT_MAX); ac->setCheckable(true); if (activeStreams.contains(INT_MAX)) { ac->setChecked(true); } m_streamAction->setVisible(true); } else { m_streamAction->setVisible(false); } } else { m_streamAction->setVisible(false); //m_audioChannels->menuAction()->setVisible(false); } connect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } if (m_controller->isReady()) { m_timePos->setRange(0, (int)m_controller->frameDuration() - 1); m_glMonitor->setRulerInfo((int)m_controller->frameDuration() - 1, controller->getMarkerModel()); loadQmlScene(MonitorSceneDefault); updateMarkers(); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection); if (out == -1) { m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false); } else { m_glMonitor->getControllerProxy()->setZone(in, out + 1, false); } m_snaps->addPoint((int)m_controller->frameDuration() - 1); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; if (!m_controller->hasVideo() || KdenliveSettings::displayClipMonitorInfo() & 0x10) { - m_glMonitor->getControllerProxy()->setAudioThumb(m_audioMeterWidget->audioChannels == 0 ? QUrl() : ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); + m_glMonitor->getControllerProxy()->setAudioThumb(m_audioMeterWidget->audioChannels == 0 ? QList() : ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } m_controller->getMarkerModel()->registerSnapModel(m_snaps); m_glMonitor->getControllerProxy()->setClipProperties(controller->clipId().toInt(), controller->clipType(), controller->hasAudioAndVideo(), controller->clipName()); m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); } else { qDebug()<<"*************** CONTROLLER NOT READY"; } // hasEffects = controller->hasEffects(); } else { loadQmlScene(MonitorSceneDefault); m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->getControllerProxy()->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; m_glMonitor->getControllerProxy()->setClipProperties(-1, ClipType::Unknown, false, QString()); //m_audioChannels->menuAction()->setVisible(false); m_streamAction->setVisible(false); } if (slotActivateMonitor()) { start(); } checkOverlay(); } void Monitor::reloadActiveStream() { if (m_controller) { QList acts = m_audioChannels->actions(); QSignalBlocker bk(m_audioChannels); QList activeStreams = m_controller->activeStreams().keys(); QMap streams = m_controller->audioStreams(); qDebug()<<"==== REFRESHING MONITOR STREAMS: "< 1) { + m_glMonitor->getControllerProxy()->setAudioStream(i18n("%1 audio streams", activeStreams.size())); + // TODO: Mix audio channels + } else { + m_glMonitor->getControllerProxy()->setAudioStream(m_controller->activeStreams().first()); + } + prepareAudioThumb(); for (auto ac : acts) { int val = ac->data().toInt(); if (streams.contains(val)) { // Update stream name in case of renaming ac->setText(streams.value(val)); } if (activeStreams.contains(val)) { ac->setChecked(true); - if (!displayedStream) { - m_glMonitor->getControllerProxy()->setAudioStream(ac->text().remove(QLatin1Char('&'))); - displayedStream = true; - } } else { ac->setChecked(false); } } } } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { m_glMonitor->setProducer(file); // render->loadUrl(file); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { // TODO or deprecate Q_UNUSED(profile) m_timePos->updateTimeCode(tc); if (/* DISABLES CODE */ (true)) { return; } slotActivateMonitor(); // render->prepareProfileReset(tc.fps()); // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(pCore->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); } void Monitor::resetConsumer(bool fullReset) { m_glMonitor->resetConsumer(fullReset); } const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone(const QPoint zone) { if (m_controller == nullptr) { return; } m_controller->setZone(zone); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); if (code == 0x01) { // Hide/show ruler m_glMonitor->switchRuler(currentOverlay & 0x01); } } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { return m_glMonitor->getControllerProxy()->zone(); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit updateScene(); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary, QVariant sceneData) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } else if (m_qmlManager->sceneType() == MonitorSplitTrack) { // Don't show another scene type if multitrack mode is active loadQmlScene(MonitorSplitTrack, sceneData); return; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType, sceneData); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return {}; } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } /*if (volume == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); }*/ } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { if (!m_glMonitor->checkFrameNumber(frame.get_position(), m_offset, m_playAction->isActive())) { m_playAction->setActive(false); } m_monitorManager->frameDisplayed(frame); } void Monitor::checkDrops() { int dropped = m_glMonitor->droppedFrames(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); } else { m_glMonitor->resetDrops(); dropped = pCore->getCurrentFps() - dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(dropped, 'g', 2)); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { bool ok = false; QDir dir = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); if (ok) { QString url = dir.absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(pCore->getCurrentFps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } } return QString(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QQuickItem *root = m_glMonitor->rootObject(); if (root) { QMetaObject::invokeMethod(root, "updatePalette"); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); m_splitEffect.reset(); loadQmlScene(MonitorSceneDefault); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active qDebug() << " . . . .. ALREADY ACTIVE"; return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { // TODO m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), position()); m_splitEffect.reset(); m_splitProducer.reset(); loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(pCore->getCurrentProfile()->profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { m_splitEffect.reset(); pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(pCore->getCurrentProfile()->profile()); std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original)); // Delete all effects int ct = 0; Mlt::Filter *filter = clone->filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (clone->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = clone->filter(ct); } trac.set_track(*original, 0); trac.set_track(*clone.get(), 1); clone.get()->attach(*m_splitEffect.get()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete original; m_splitProducer = std::make_shared(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type, QVariant sceneData) { if (m_id == Kdenlive::DvdMonitor) { m_qmlManager->setScene(m_id, MonitorSceneDefault, pCore->getCurrentFrameSize(), pCore->getCurrentDar(), m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); return; } if (type == m_qmlManager->sceneType() && sceneData.isNull()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500); type = MonitorSceneDefault; } m_qmlManager->setScene(m_id, type, pCore->getCurrentFrameSize(), pCore->getCurrentDar(), m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); QQuickItem *root = m_glMonitor->rootObject(); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; case MonitorSplitTrack: m_qmlManager->setProperty(QStringLiteral("tracks"), sceneData); break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = root->property("percentage").toDouble(); // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", 0.5 - (percent - 0.5) * .666); } m_glMonitor->refresh(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } void Monitor::prepareAudioThumb() { if (m_controller) { m_glMonitor->getControllerProxy()->setAudioThumb(ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); if (isActive && m_glWidget->isFullScreen()) { // If both monitors are fullscreen, this is necessary to do the switch m_glWidget->showFullScreen(); m_videoWidget->setFocus(); } } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); bool showDropped = currentOverlay & 0x20; m_glMonitor->rootObject()->setProperty("showFps", showDropped); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); if (showDropped) { if (!m_droppedTimer.isActive() && m_playAction->isActive()) { m_glMonitor->resetDrops(); m_droppedTimer.start(); } } else { m_droppedTimer.stop(); } } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::processSeek(int pos) { slotActivateMonitor(); pause(); m_glMonitor->requestSeek(pos); m_monitorManager->cleanMixer(); } void Monitor::requestSeek(int pos) { m_glMonitor->getControllerProxy()->setPosition(pos); } void Monitor::setProducer(std::shared_ptr producer, int pos) { m_glMonitor->setProducer(std::move(producer), isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } void Monitor::slotSeekPosition(int pos) { emit seekPosition(pos); m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->getControllerProxy()->setPosition(0); resetSpeedInfo(); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); resetSpeedInfo(); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->duration() - 1); } else { m_glMonitor->getControllerProxy()->setPosition(pCore->projectDuration() - 1); } } void Monitor::resetSpeedInfo() { m_speedIndex = -1; m_speedLabel->setFixedWidth(0); m_speedLabel->clear(); } void Monitor::addSnapPoint(int pos) { m_snaps->addPoint(pos); } void Monitor::removeSnapPoint(int pos) { m_snaps->removePoint(pos); } void Monitor::slotSetScreen(int screenIndex) { emit screenChanged(screenIndex); } void Monitor::slotZoomIn() { m_glMonitor->slotZoom(true); } void Monitor::slotZoomOut() { m_glMonitor->slotZoom(false); } void Monitor::setConsumerProperty(const QString &name, const QString &value) { m_glMonitor->setConsumerProperty(name, value); } void Monitor::purgeCache() { m_glMonitor->purgeCache(); } void Monitor::updateBgColor() { m_glMonitor->m_bgColor = KdenliveSettings::window_background(); } MonitorProxy *Monitor::getControllerProxy() { return m_glMonitor->getControllerProxy(); } void Monitor::updateMultiTrackView(int tid) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("activeTrack", tid); } } diff --git a/src/monitor/monitorproxy.cpp b/src/monitor/monitorproxy.cpp index 698fd84da..9c4026516 100644 --- a/src/monitor/monitorproxy.cpp +++ b/src/monitor/monitorproxy.cpp @@ -1,348 +1,348 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "monitorproxy.h" #include "core.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitormanager.h" #include "profiles/profilemodel.hpp" #include #include #include #include MonitorProxy::MonitorProxy(GLWidget *parent) : QObject(parent) , q(parent) , m_position(0) , m_zoneIn(0) , m_zoneOut(-1) , m_hasAV(false) , m_clipType(0) , m_clipId(-1) , m_seekFinished(true) { } int MonitorProxy::getPosition() const { return m_position; } int MonitorProxy::rulerHeight() const { return q->m_rulerHeight; } int MonitorProxy::overlayType() const { return (q->m_id == (int)Kdenlive::ClipMonitor ? KdenliveSettings::clipMonitorOverlayGuides() : KdenliveSettings::projectMonitorOverlayGuides()); } void MonitorProxy::setOverlayType(int ix) { if (q->m_id == (int)Kdenlive::ClipMonitor) { KdenliveSettings::setClipMonitorOverlayGuides(ix); } else { KdenliveSettings::setProjectMonitorOverlayGuides(ix); } } QString MonitorProxy::markerComment() const { return m_markerComment; } bool MonitorProxy::setPosition(int pos) { if (m_position == pos) { return true; } m_position = pos; emit requestSeek(pos); if (m_seekFinished) { m_seekFinished = false; emit seekFinishedChanged(); } emit positionChanged(pos); return false; } void MonitorProxy::positionFromConsumer(int pos, bool playing) { if (playing) { m_position = pos; emit positionChanged(pos); if (!m_seekFinished) { m_seekFinished = true; emit seekFinishedChanged(); } } else { if (!m_seekFinished && m_position == pos) { m_seekFinished = true; emit seekFinishedChanged(); } } } void MonitorProxy::setMarkerComment(const QString &comment) { if (m_markerComment == comment) { return; } m_markerComment = comment; emit markerCommentChanged(); } int MonitorProxy::zoneIn() const { return m_zoneIn; } int MonitorProxy::zoneOut() const { return m_zoneOut; } void MonitorProxy::setZoneIn(int pos) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } if (pos > m_zoneOut) { if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneOut = qMin(q->duration(), pos + (m_zoneOut - m_zoneIn)); emit addSnap(m_zoneOut - 1); } m_zoneIn = pos; if (pos > 0) { emit addSnap(pos); } emit zoneChanged(); emit saveZone(QPoint(m_zoneIn, m_zoneOut)); } void MonitorProxy::setZoneOut(int pos) { if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } if (pos < m_zoneIn) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } m_zoneIn = qMax(0, pos - (m_zoneOut - m_zoneIn)); emit addSnap(m_zoneIn); } m_zoneOut = pos; if (pos > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); emit saveZone(QPoint(m_zoneIn, m_zoneOut)); } void MonitorProxy::startZoneMove() { m_undoZone = QPoint(m_zoneIn, m_zoneOut); } void MonitorProxy::endZoneMove() { emit saveZoneWithUndo(m_undoZone, QPoint(m_zoneIn, m_zoneOut)); } void MonitorProxy::setZone(int in, int out, bool sendUpdate) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneIn = in; m_zoneOut = out; if (m_zoneIn > 0) { emit addSnap(m_zoneIn); } if (m_zoneOut > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); if (sendUpdate) { emit saveZone(QPoint(m_zoneIn, m_zoneOut)); } } void MonitorProxy::setZone(QPoint zone, bool sendUpdate) { setZone(zone.x(), zone.y(), sendUpdate); } void MonitorProxy::resetZone() { m_zoneIn = 0; m_zoneOut = -1; } double MonitorProxy::fps() const { return pCore->getCurrentFps(); } QPoint MonitorProxy::zone() const { return {m_zoneIn, m_zoneOut}; } QImage MonitorProxy::extractFrame(int frame_position, const QString &path, int width, int height, bool useSourceProfile) { if (width == -1) { width = pCore->getCurrentProfile()->width(); height = pCore->getCurrentProfile()->height(); } else if (width % 2 == 1) { width++; } if (!path.isEmpty()) { QScopedPointer tmpProfile(new Mlt::Profile()); QScopedPointer producer(new Mlt::Producer(*tmpProfile, path.toUtf8().constData())); if (producer && producer->is_valid()) { tmpProfile->from_producer(*producer); width = tmpProfile->width(); height = tmpProfile->height(); double projectFps = pCore->getCurrentFps(); double currentFps = tmpProfile->fps(); if (!qFuzzyCompare(projectFps, currentFps)) { frame_position = frame_position * currentFps / projectFps; } QImage img = KThumb::getFrame(producer.data(), frame_position, width, height); return img; } } if ((q->m_producer == nullptr) || !path.isEmpty()) { QImage pix(width, height, QImage::Format_RGB32); pix.fill(Qt::black); return pix; } Mlt::Frame *frame = nullptr; QImage img; if (useSourceProfile) { // Our source clip's resolution is higher than current profile, export at full res QScopedPointer tmpProfile(new Mlt::Profile()); QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd(new Mlt::Producer(*tmpProfile, service.toUtf8().constData(), q->m_producer->get("resource"))); tmpProfile->from_producer(*tmpProd); width = tmpProfile->width(); height = tmpProfile->height(); if (tmpProd && tmpProd->is_valid()) { Mlt::Filter scaler(*tmpProfile, "swscale"); Mlt::Filter converter(*tmpProfile, "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); // TODO: paste effects // Clip(*tmpProd).addEffects(*q->m_producer); double projectFps = pCore->getCurrentFps(); double currentFps = tmpProfile->fps(); if (qFuzzyCompare(projectFps, currentFps)) { tmpProd->seek(q->m_producer->position()); } else { tmpProd->seek(q->m_producer->position() * currentFps / projectFps); } frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } } else if (KdenliveSettings::gpu_accel()) { QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd( new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), q->m_producer->get("resource"))); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); tmpProd->seek(q->m_producer->position()); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } else { frame = q->m_producer->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } return img; } void MonitorProxy::activateClipMonitor(bool isClipMonitor) { pCore->monitorManager()->activateMonitor(isClipMonitor ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor); } QString MonitorProxy::toTimecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : q->frameToTime(frames); } void MonitorProxy::setClipProperties(int clipId, ClipType::ProducerType type, bool hasAV, const QString clipName) { if (hasAV != m_hasAV) { m_hasAV = hasAV; emit clipHasAVChanged(); } if (clipName == m_clipName) { m_clipName.clear(); emit clipNameChanged(); } m_clipName = clipName; emit clipNameChanged(); if (type != m_clipType) { m_clipType = type; emit clipTypeChanged(); } if (clipId != m_clipId) { m_clipId = clipId; emit clipIdChanged(); } } -void MonitorProxy::setAudioThumb(const QUrl thumbPath) +void MonitorProxy::setAudioThumb(const QList thumbPath) { m_audioThumb = thumbPath; emit audioThumbChanged(); } void MonitorProxy::setAudioStream(const QString &name) { m_clipStream = name; emit clipStreamChanged(); } QPoint MonitorProxy::profile() { QSize s = pCore->getCurrentFrameSize(); return QPoint(s.width(), s.height()); } diff --git a/src/monitor/monitorproxy.h b/src/monitor/monitorproxy.h index 91cae4259..692ac447a 100644 --- a/src/monitor/monitorproxy.h +++ b/src/monitor/monitorproxy.h @@ -1,138 +1,138 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 . * ***************************************************************************/ /** @brief This class is a wrapper around the monitor / glwidget and handles communication * with the qml overlay through its properties. */ #ifndef MONITORPROXY_H #define MONITORPROXY_H #include "definitions.h" #include #include #include class GLWidget; class MonitorProxy : public QObject { Q_OBJECT // Q_PROPERTY(int consumerPosition READ consumerPosition NOTIFY consumerPositionChanged) Q_PROPERTY(int position MEMBER m_position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(QPoint profile READ profile NOTIFY profileChanged) Q_PROPERTY(int seekFinished MEMBER m_seekFinished NOTIFY seekFinishedChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int rulerHeight READ rulerHeight NOTIFY rulerHeightChanged) Q_PROPERTY(QString markerComment READ markerComment NOTIFY markerCommentChanged) - Q_PROPERTY(QUrl audioThumb MEMBER m_audioThumb NOTIFY audioThumbChanged) + Q_PROPERTY(QList audioThumb MEMBER m_audioThumb NOTIFY audioThumbChanged) Q_PROPERTY(int overlayType READ overlayType WRITE setOverlayType NOTIFY overlayTypeChanged) /** @brief: Returns true if current clip in monitor has Audio and Video * */ Q_PROPERTY(bool clipHasAV MEMBER m_hasAV NOTIFY clipHasAVChanged) /** @brief: Contains the name of clip currently displayed in monitor * */ Q_PROPERTY(QString clipName MEMBER m_clipName NOTIFY clipNameChanged) Q_PROPERTY(QString clipStream MEMBER m_clipStream NOTIFY clipStreamChanged) /** @brief: Contains the name of clip currently displayed in monitor * */ Q_PROPERTY(int clipType MEMBER m_clipType NOTIFY clipTypeChanged) Q_PROPERTY(int clipId MEMBER m_clipId NOTIFY clipIdChanged) public: MonitorProxy(GLWidget *parent); /** brief: Returns true if we are still in a seek operation * */ int rulerHeight() const; int overlayType() const; void setOverlayType(int ix); QString markerComment() const; /** brief: update position and end seeking if we reached the requested seek position. * returns true if the position was unchanged, false otherwise * */ int getPosition() const; Q_INVOKABLE bool setPosition(int pos); void positionFromConsumer(int pos, bool playing); void setMarkerComment(const QString &comment); int zoneIn() const; int zoneOut() const; void setZoneIn(int pos); void setZoneOut(int pos); Q_INVOKABLE void setZone(int in, int out, bool sendUpdate = true); /** brief: Activate clip monitor if param is true, project monitor otherwise * */ Q_INVOKABLE void activateClipMonitor(bool isClipMonitor); void setZone(QPoint zone, bool sendUpdate = true); void resetZone(); QPoint zone() const; QImage extractFrame(int frame_position, const QString &path = QString(), int width = -1, int height = -1, bool useSourceProfile = false); Q_INVOKABLE QString toTimecode(int frames) const; Q_INVOKABLE void startZoneMove(); Q_INVOKABLE void endZoneMove(); Q_INVOKABLE double fps() const; QPoint profile(); void setClipProperties(int clipId, ClipType::ProducerType type, bool hasAV, const QString clipName); - void setAudioThumb(const QUrl thumbPath = QUrl()); + void setAudioThumb(const QList thumbPath = QList ()); void setAudioStream(const QString &name); signals: void positionChanged(int); void seekFinishedChanged(); void requestSeek(int pos); void zoneChanged(); void saveZone(const QPoint zone); void saveZoneWithUndo(const QPoint, const QPoint&); void markerCommentChanged(); void rulerHeightChanged(); void addSnap(int); void removeSnap(int); void triggerAction(const QString &name); void overlayTypeChanged(); void seekNextKeyframe(); void seekPreviousKeyframe(); void addRemoveKeyframe(); void seekToKeyframe(); void clipHasAVChanged(); void clipNameChanged(); void clipStreamChanged(); void clipTypeChanged(); void clipIdChanged(); void audioThumbChanged(); void profileChanged(); private: GLWidget *q; int m_position; int m_zoneIn; int m_zoneOut; bool m_hasAV; - QUrl m_audioThumb; + QList m_audioThumb; QString m_markerComment; QString m_clipName; QString m_clipStream; int m_clipType; int m_clipId; bool m_seekFinished; QPoint m_undoZone; }; #endif diff --git a/src/monitor/view/kdenliveclipmonitor.qml b/src/monitor/view/kdenliveclipmonitor.qml index 5022c91a0..8b3d337e8 100644 --- a/src/monitor/view/kdenliveclipmonitor.qml +++ b/src/monitor/view/kdenliveclipmonitor.qml @@ -1,395 +1,413 @@ import QtQuick.Controls 2.4 import QtQuick.Window 2.2 import Kdenlive.Controls 1.0 import QtQuick 2.11 import com.enums 1.0 Item { id: root objectName: "root" SystemPalette { id: activePalette } // default size, but scalable by user height: 300; width: 400 property string markerText property int itemType: 0 property point profile: controller.profile property double zoom property double scalex property double scaley property bool dropped: false property string fps: '-' property bool showMarkers: false property bool showTimecode: false property bool showFps: false property bool showSafezone: false property bool showAudiothumb: false property bool showToolbar: false property string clipName: controller.clipName property real baseUnit: fontMetrics.font.pixelSize property int duration: 300 property int mouseRulerPos: 0 property double frameSize: 10 property double timeScale: 1 property int overlayType: controller.overlayType property color overlayColor: 'cyan' property bool isClipMonitor: true property int dragType: 0 FontMetrics { id: fontMetrics font: fixedFont } signal editCurrentMarker() onDurationChanged: { clipMonitorRuler.updateRuler() } onWidthChanged: { clipMonitorRuler.updateRuler() } onClipNameChanged: { // Animate clip name clipNameLabel.opacity = 1 showAnimate.restart() } function updatePalette() { clipMonitorRuler.forceRepaint() } function switchOverlay() { if (controller.overlayType >= 5) { controller.overlayType = 0 } else { controller.overlayType = controller.overlayType + 1; } root.overlayType = controller.overlayType } MouseArea { id: barOverArea hoverEnabled: true acceptedButtons: Qt.NoButton anchors.fill: parent } SceneToolBar { id: sceneToolBar barContainsMouse: sceneToolBar.rightSide ? barOverArea.mouseX >= x - 10 : barOverArea.mouseX < x + width + 10 onBarContainsMouseChanged: { sceneToolBar.opacity = 1 sceneToolBar.visible = sceneToolBar.barContainsMouse } anchors { right: parent.right top: parent.top topMargin: 4 rightMargin: 4 leftMargin: 4 } } Item { height: root.height - controller.rulerHeight width: root.width Item { id: frame objectName: "referenceframe" width: root.profile.x * root.scalex height: root.profile.y * root.scaley anchors.centerIn: parent Loader { anchors.fill: parent source: { switch(root.overlayType) { case 0: return ''; case 1: return "OverlayStandard.qml"; case 2: return "OverlayMinimal.qml"; case 3: return "OverlayCenter.qml"; case 4: return "OverlayCenterDiagonal.qml"; case 5: return "OverlayThirds.qml"; } } } } Item { id: monitorOverlay anchors.fill: parent Item { id: audioThumb property bool stateVisible: (clipMonitorRuler.containsMouse || thumbMouseArea.containsMouse) property bool isAudioClip: controller.clipType == ProducerType.Audio anchors { left: parent.left bottom: parent.bottom } Label { id: clipStreamLabel font: fixedFont anchors { bottom: audioThumb.isAudioClip ? parent.bottom : parent.top horizontalCenter: parent.horizontalCenter } color: "white" text: controller.clipStream background: Rectangle { color: "#222277" } visible: text != "" padding :4 } height: isAudioClip ? parent.height : parent.height / 6 //font.pixelSize * 3 width: parent.width visible: root.showAudiothumb && (isAudioClip || controller.clipType == ProducerType.AV) states: [ State { when: audioThumb.stateVisible || audioThumb.isAudioClip; PropertyChanges { target: audioThumb; opacity: 1.0 } }, State { when: !audioThumb.stateVisible && !audioThumb.isAudioClip; PropertyChanges { target: audioThumb; opacity: 0.0 } } ] transitions: [ Transition { NumberAnimation { property: "opacity"; duration: audioThumb.isAudioClip ? 0 : 500} } ] Rectangle { color: activePalette.base opacity: audioThumb.isAudioClip ? 1 : 0.6 anchors.fill: parent } Rectangle { color: "yellow" opacity: 0.3 height: parent.height x: controller.zoneIn * timeScale width: (controller.zoneOut - controller.zoneIn) * timeScale visible: controller.zoneIn > 0 || controller.zoneOut < duration - 1 } - Image { - anchors.fill: parent - source: controller.audioThumb - asynchronous: true + Repeater { + id: streamThumb + model: controller.audioThumb.length + property double streamHeight: parent.height / streamThumb.count + Item { + anchors.fill: parent + Image { + anchors.left: parent.left + anchors.right: parent.right + height: streamThumb.streamHeight + y: model.index * height + source: controller.audioThumb[model.index] + asynchronous: true + } + Rectangle { + width: parent.width + y: (model.index + 1) * streamThumb.streamHeight + height: 1 + visible: streamThumb.count > 1 && model.index < streamThumb.count - 1 + color: 'black' + } + } } Rectangle { color: "red" width: 1 height: parent.height x: controller.position * timeScale } MouseArea { id: thumbMouseArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true onPositionChanged: { if (mouse.modifiers & Qt.ShiftModifier) { var pos = Math.max(mouseX, 0) controller.setPosition(Math.min(pos / root.timeScale, root.duration)); } } } } Label { id: clipNameLabel font: fixedFont anchors { top: parent.top horizontalCenter: parent.horizontalCenter } color: "white" text: clipName background: Rectangle { color: "#222277" } visible: clipName != "" padding :4 SequentialAnimation { id: showAnimate running: false NumberAnimation { target: clipNameLabel; duration: 3000 } NumberAnimation { target: clipNameLabel; property: "opacity"; to: 0; duration: 1000 } } } Label { id: timecode font.family: fontMetrics.font.family font.pointSize: 1.5 * fontMetrics.font.pointSize objectName: "timecode" color: "#ffffff" padding: 2 background: Rectangle { color: "#66000000" } text: controller.toTimecode(controller.position) visible: root.showTimecode anchors { right: parent.right bottom: parent.bottom bottomMargin: (audioThumb.stateVisible && !audioThumb.isAudioClip && audioThumb.visible) ? audioThumb.height : 0 } } Label { id: fpsdropped font.family: fontMetrics.font.family font.pointSize: 1.5 * fontMetrics.font.pointSize objectName: "fpsdropped" color: "#ffffff" padding: 2 background: Rectangle { color: root.dropped ? "#99ff0000" : "#66004400" } text: i18n("%1fps", root.fps) visible: root.showFps anchors { right: timecode.visible ? timecode.left : parent.right bottom: parent.bottom bottomMargin: (audioThumb.stateVisible && !audioThumb.isAudioClip && audioThumb.visible) ? audioThumb.height : 0 } } Label { id: inPoint font: fixedFont anchors { left: parent.left bottom: parent.bottom } visible: root.showMarkers && controller.position == controller.zoneIn text: i18n("In Point") color: "white" background: Rectangle { color: "#228b22" } padding:4 horizontalAlignment: TextInput.AlignHCenter } Label { id: outPoint font: fixedFont anchors { left: inPoint.visible ? inPoint.right : parent.left bottom: parent.bottom } visible: root.showMarkers && controller.position + 1 == controller.zoneOut text: i18n("Out Point") color: "white" background: Rectangle { color: "#770000" } padding: 4 horizontalAlignment: TextInput.AlignHCenter } TextField { id: marker font: fixedFont objectName: "markertext" activeFocusOnPress: true text: controller.markerComment onEditingFinished: { root.markerText = marker.displayText marker.focus = false root.editCurrentMarker() } anchors { left: outPoint.visible ? outPoint.right : inPoint.visible ? inPoint.right : parent.left bottom: parent.bottom } visible: root.showMarkers && text != "" height: inPoint.height width: fontMetrics.boundingRect(displayText).width + 10 horizontalAlignment: displayText == text ? TextInput.AlignHCenter : TextInput.AlignLeft background: Rectangle { color: "#990000ff" } color: "#ffffff" padding: 0 maximumLength: 20 } } Rectangle { // Audio or video only drag zone x: 2 y: inPoint.visible || outPoint.visible || marker.visible ? parent.height - inPoint.height - height - 2 : parent.height - height - 2 width: childrenRect.width height: childrenRect.height color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.7) radius: 4 opacity: (dragAudioArea.containsMouse || dragVideoArea.containsMouse || thumbMouseArea.containsMouse || (barOverArea.containsMouse && barOverArea.mouseY >= y)) ? 1 : 0 visible: controller.clipHasAV onOpacityChanged: { if (opacity == 1) { videoDragButton.x = 0 videoDragButton.y = 0 audioDragButton.x = videoDragButton.x + videoDragButton.width audioDragButton.y = 0 } } Row { id: dragRow ToolButton { id: videoDragButton icon.name: "kdenlive-show-video" Drag.active: dragVideoArea.drag.active Drag.dragType: Drag.Automatic Drag.mimeData: { "kdenlive/producerslist" : "V" + controller.clipId + "/" + controller.zoneIn + "/" + (controller.zoneOut - 1) } MouseArea { id: dragVideoArea hoverEnabled: true anchors.fill: parent propagateComposedEvents: true cursorShape: Qt.PointingHand drag.target: parent onExited: { parent.x = 0 parent.y = 0 } } } ToolButton { id: audioDragButton icon.name: "audio-volume-medium" Drag.active: dragAudioArea.drag.active Drag.dragType: Drag.Automatic Drag.mimeData: { "kdenlive/producerslist" : "A" + controller.clipId + "/" + controller.zoneIn + "/" + (controller.zoneOut - 1) } MouseArea { id: dragAudioArea hoverEnabled: true anchors.fill: parent propagateComposedEvents: true cursorShape: Qt.PointingHand drag.target: parent onExited: { parent.x = videoDragButton.x + videoDragButton.width parent.y = 0 } } } } } } MonitorRuler { id: clipMonitorRuler anchors { left: root.left right: root.right bottom: root.bottom } height: controller.rulerHeight } } diff --git a/src/utils/thumbnailcache.cpp b/src/utils/thumbnailcache.cpp index b3de9eb53..54017e48c 100644 --- a/src/utils/thumbnailcache.cpp +++ b/src/utils/thumbnailcache.cpp @@ -1,298 +1,325 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "thumbnailcache.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include #include #include std::unique_ptr ThumbnailCache::instance; std::once_flag ThumbnailCache::m_onceFlag; class ThumbnailCache::Cache_t { public: Cache_t(int maxCost) : m_maxCost(maxCost) { } bool contains(const QString &key) const { return m_cache.count(key) > 0; } void remove(const QString &key) { if (!contains(key)) { return; } auto it = m_cache.at(key); m_currentCost -= (*it).second.second; m_data.erase(it); m_cache.erase(key); } void insert(const QString &key, const QImage &img, int cost) { if (cost > m_maxCost) { return; } m_data.push_front({key, {img, cost}}); auto it = m_data.begin(); m_cache[key] = it; m_currentCost += cost; while (m_currentCost > m_maxCost) { remove(m_data.back().first); } } QImage get(const QString &key) { if (!contains(key)) { return QImage(); } // when a get operation occurs, we put the corresponding list item in front to remember last access std::pair> data; auto it = m_cache.at(key); std::swap(data, (*it)); // take data out without copy QImage result = data.second.first; // a copy occurs here m_data.erase(it); // delete old iterator m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator return result; } void clear() { m_data.clear(); m_cache.clear(); m_currentCost = 0; } protected: int m_maxCost; int m_currentCost{0}; std::list>> m_data; // the data is stored as (key,(image, cost)) std::unordered_map m_cache; }; ThumbnailCache::ThumbnailCache() : m_volatileCache(new Cache_t(10000000)) { } std::unique_ptr &ThumbnailCache::get() { std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); }); return instance; } bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const { QMutexLocker locker(&m_mutex); bool ok = false; - auto key = pos < 0 ? getAudioKey(binId, &ok) : getKey(binId, pos, &ok); + auto key = pos < 0 ? getAudioKey(binId, &ok).first() : getKey(binId, pos, &ok); if (ok && m_volatileCache->contains(key)) { return true; } if (!ok || volatileOnly) { return false; } QDir thumbFolder = getDir(pos < 0, &ok); return ok && thumbFolder.exists(key); } QImage ThumbnailCache::getAudioThumbnail(const QString &binId, bool volatileOnly) const { QMutexLocker locker(&m_mutex); bool ok = false; - auto key = getAudioKey(binId, &ok); + auto key = getAudioKey(binId, &ok).first(); if (ok && m_volatileCache->contains(key)) { return m_volatileCache->get(key); } if (!ok || volatileOnly) { return QImage(); } QDir thumbFolder = getDir(true, &ok); if (ok && thumbFolder.exists(key)) { m_storedOnDisk[binId].push_back(-1); return QImage(thumbFolder.absoluteFilePath(key)); } return QImage(); } -const QUrl ThumbnailCache::getAudioThumbPath(const QString &binId) const +const QList ThumbnailCache::getAudioThumbPath(const QString &binId) const { QMutexLocker locker(&m_mutex); bool ok = false; auto key = getAudioKey(binId, &ok); QDir thumbFolder = getDir(true, &ok); - if (ok && thumbFolder.exists(key)) { - return QUrl::fromLocalFile(thumbFolder.absoluteFilePath(key)); + QList pathList; + if (ok) { + for (const QString &p : key) { + if (thumbFolder.exists(p)) { + pathList <contains(key)) { return m_volatileCache->get(key); } if (!ok || volatileOnly) { return QImage(); } QDir thumbFolder = getDir(false, &ok); if (ok && thumbFolder.exists(key)) { m_storedOnDisk[binId].push_back(pos); return QImage(thumbFolder.absoluteFilePath(key)); } return QImage(); } void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent) { QMutexLocker locker(&m_mutex); bool ok = false; const QString key = getKey(binId, pos, &ok); if (!ok) { return; } if (persistent) { QDir thumbFolder = getDir(false, &ok); if (ok) { if (!img.save(thumbFolder.absoluteFilePath(key))) { qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: "<contains(key)) { m_volatileCache->remove(key); } else { m_storedVolatile[binId].push_back(pos); } m_volatileCache->insert(key, img, (int)img.sizeInBytes()); } } else { m_volatileCache->insert(key, img, (int)img.sizeInBytes()); m_storedVolatile[binId].push_back(pos); } } void ThumbnailCache::saveCachedThumbs(QStringList keys) { bool ok; QDir thumbFolder = getDir(false, &ok); if (!ok) { return; } for (const QString &key : keys) { if (!thumbFolder.exists(key) && m_volatileCache->contains(key)) { QImage img = m_volatileCache->get(key); if (!img.save(thumbFolder.absoluteFilePath(key))) { qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath(); break; } } } } void ThumbnailCache::invalidateThumbsForClip(const QString &binId, bool reloadAudio) { QMutexLocker locker(&m_mutex); if (m_storedVolatile.find(binId) != m_storedVolatile.end()) { bool ok = false; for (int pos : m_storedVolatile.at(binId)) { auto key = getKey(binId, pos, &ok); if (ok) { m_volatileCache->remove(key); } } m_storedVolatile.erase(binId); } bool ok = false; // Video thumbs QDir thumbFolder = getDir(false, &ok); QDir audioThumbFolder = getDir(true, &ok); if (ok && m_storedOnDisk.find(binId) != m_storedOnDisk.end()) { // Remove persistent cache for (int pos : m_storedOnDisk.at(binId)) { if (pos < 0) { if (reloadAudio) { auto key = getAudioKey(binId, &ok); if (ok) { - QFile::remove(audioThumbFolder.absoluteFilePath(key)); + for (const QString &p : key) { + QFile::remove(audioThumbFolder.absoluteFilePath(p)); + } } } } else { auto key = getKey(binId, pos, &ok); if (ok) { QFile::remove(thumbFolder.absoluteFilePath(key)); } } } m_storedOnDisk.erase(binId); } } void ThumbnailCache::clearCache() { QMutexLocker locker(&m_mutex); m_volatileCache->clear(); m_storedVolatile.clear(); m_storedOnDisk.clear(); } // static QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok) { if (binId.isEmpty()) { *ok = false; return QString(); } auto binClip = pCore->projectItemModel()->getClipByBinID(binId); *ok = binClip != nullptr; return *ok ? binClip->hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png") : QString(); } // static -QString ThumbnailCache::getAudioKey(const QString &binId, bool *ok) +QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); *ok = binClip != nullptr; if (ok) { - int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index")); - if (audio > -1) { - return binClip->hash() + QLatin1Char('_') + QString::number(audio) + QStringLiteral(".png"); + QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams")); + if (streams.isEmpty()) { + // activate all audio streams + QList streamIxes = binClip->audioStreams().keys(); + if (streamIxes.size() > 1) { + QStringList streamsList; + for (const int st : streamIxes) { + streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st); + } + return streamsList; + } + } + if (streams.size() == 1) { + int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index")); + if (audio > -1) { + return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)}; + } + return {binClip->hash() + QStringLiteral(".png")}; + } + QStringList streamsList; + QStringList streamIndexes = streams.split(QLatin1Char(';')); + for (const QString st : streamIndexes) { + streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st); } - return binClip->hash() + QStringLiteral(".png"); + return streamsList; } - return QString(); + return {}; } // static QDir ThumbnailCache::getDir(bool audio, bool *ok) { return pCore->currentDoc()->getCacheDir(audio ? CacheAudio : CacheThumbs, ok); } diff --git a/src/utils/thumbnailcache.hpp b/src/utils/thumbnailcache.hpp index cd98b24c8..a7ab3b2f4 100644 --- a/src/utils/thumbnailcache.hpp +++ b/src/utils/thumbnailcache.hpp @@ -1,104 +1,104 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include "definitions.h" #include #include #include #include #include #include #include #include /** @brief This class class is an interface to the caches that store thumbnails. In Kdenlive, we use two such caches, a persistent that is stored on disk to allow thumbnails to be reused when reopening. The other one is a volatile LRU cache that lives in memory. Note that for the volatile cache uses a custom implementation. QCache is not suitable since it operates on pointers and since the object is removed from the cache when accessed. KImageCache is not suitable since it lacks a way to remove objects from the cache. * Note that this class is a Singleton */ class ThumbnailCache { public: // Returns the instance of the Singleton static std::unique_ptr &get(); /* @brief Check whether a given thumbnail is in the cache @param binId is the id of the queried clip @param pos is the position where we query @param volatileOnly if true, we only check the volatile cache (no disk access) */ bool hasThumbnail(const QString &binId, int pos, bool volatileOnly = false) const; /* @brief Get a given thumbnail from the cache @param binId is the id of the queried clip @param pos is the position where we query @param volatileOnly if true, we only check the volatile cache (no disk access) */ QImage getThumbnail(const QString &binId, int pos, bool volatileOnly = false) const; QImage getAudioThumbnail(const QString &binId, bool volatileOnly = false) const; - const QUrl getAudioThumbPath(const QString &binId) const; + const QList getAudioThumbPath(const QString &binId) const; /* @brief Get a given thumbnail from the cache @param binId is the id of the queried clip @param pos is the position where we query @param persistent if true, we store the image in the persistent cache, which generates a disk access */ void storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent = false); /* @brief Removes all the thumbnails for a given clip */ void invalidateThumbsForClip(const QString &binId, bool reloadAudio); /* @brief Save all cached thumbs to disk */ void saveCachedThumbs(QStringList keys); /* @brief Reset cache (discarding all thumbs stored in memory) */ void clearCache(); protected: // Constructor is protected because class is a Singleton ThumbnailCache(); // Return the key associated to a thumbnail static QString getKey(const QString &binId, int pos, bool *ok); - static QString getAudioKey(const QString &binId, bool *ok); + static QStringList getAudioKey(const QString &binId, bool *ok); // Return the dir where the persistent cache lives static QDir getDir(bool audio, bool *ok); static std::unique_ptr instance; static std::once_flag m_onceFlag; // flag to create the repository only once; class Cache_t; std::unique_ptr m_volatileCache; mutable QMutex m_mutex; // the following maps keeps track of the positions that we store for each clip in volatile caches. // Note that we don't track deletions due to items dropped from the cache. So the maps can contain more items that are currently stored. std::unordered_map> m_storedVolatile; mutable std::unordered_map> m_storedOnDisk; };