diff --git a/src/assets/assetpanel.cpp b/src/assets/assetpanel.cpp
index c005e36ff..35eb1183b 100644
--- a/src/assets/assetpanel.cpp
+++ b/src/assets/assetpanel.cpp
@@ -1,381 +1,387 @@
/***************************************************************************
* 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 "assetpanel.hpp"
#include "core.h"
#include "definitions.h"
#include "effects/effectstack/model/effectitemmodel.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "effects/effectstack/view/effectstackview.hpp"
#include "kdenlivesettings.h"
#include "model/assetparametermodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include "transitions/view/transitionstackview.hpp"
#include "view/assetparameterview.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
AssetPanel::AssetPanel(QWidget *parent)
: QWidget(parent)
, m_lay(new QVBoxLayout(this))
, m_assetTitle(new KSqueezedTextLabel(this))
, m_container(new QWidget(this))
, m_transitionWidget(new TransitionStackView(this))
, m_effectStackWidget(new EffectStackView(this))
{
auto *buttonToolbar = new QToolBar(this);
m_titleAction = buttonToolbar->addWidget(m_assetTitle);
int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
QSize iconSize(size, size);
buttonToolbar->setIconSize(iconSize);
// Edit composition button
m_switchCompoButton = new QComboBox(this);
m_switchCompoButton->setFrame(false);
auto allTransitions = TransitionsRepository::get()->getNames();
for (const auto &transition : allTransitions) {
m_switchCompoButton->addItem(transition.second, transition.first);
}
connect(m_switchCompoButton, static_cast(&QComboBox::activated), [&]() {
if (m_transitionWidget->stackOwner().first == ObjectType::TimelineComposition) {
emit switchCurrentComposition(m_transitionWidget->stackOwner().second, m_switchCompoButton->currentData().toString());
}
});
m_switchCompoButton->setToolTip(i18n("Change composition type"));
m_switchAction = buttonToolbar->addWidget(m_switchCompoButton);
m_switchAction->setVisible(false);
// spacer
QWidget *empty = new QWidget();
empty->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
buttonToolbar->addWidget(empty);
m_switchBuiltStack = new QToolButton(this);
m_switchBuiltStack->setIcon(QIcon::fromTheme(QStringLiteral("adjustlevels")));
m_switchBuiltStack->setToolTip(i18n("Adjust clip"));
m_switchBuiltStack->setCheckable(true);
m_switchBuiltStack->setChecked(KdenliveSettings::showbuiltstack());
m_switchBuiltStack->setVisible(false);
// connect(m_switchBuiltStack, &QToolButton::toggled, m_effectStackWidget, &EffectStackView::switchBuiltStack);
buttonToolbar->addWidget(m_switchBuiltStack);
m_splitButton = new KDualAction(i18n("Normal view"), i18n("Compare effect"), this);
m_splitButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("view-right-close")));
m_splitButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
m_splitButton->setToolTip(i18n("Compare effect"));
m_splitButton->setVisible(false);
connect(m_splitButton, &KDualAction::activeChangedByUser, this, &AssetPanel::processSplitEffect);
buttonToolbar->addAction(m_splitButton);
m_enableStackButton = new KDualAction(i18n("Effects disabled"), i18n("Effects enabled"), this);
m_enableStackButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("hint")));
m_enableStackButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("visibility")));
connect(m_enableStackButton, &KDualAction::activeChangedByUser, this, &AssetPanel::enableStack);
m_enableStackButton->setVisible(false);
buttonToolbar->addAction(m_enableStackButton);
m_timelineButton = new KDualAction(i18n("Hide keyframes"), i18n("Display keyframes in timeline"), this);
m_timelineButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("adjustlevels")));
m_timelineButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("adjustlevels")));
m_timelineButton->setToolTip(i18n("Display keyframes in timeline"));
m_timelineButton->setVisible(false);
connect(m_timelineButton, &KDualAction::activeChangedByUser, this, &AssetPanel::showKeyframes);
buttonToolbar->addAction(m_timelineButton);
m_lay->addWidget(buttonToolbar);
m_lay->setContentsMargins(0, 0, 0, 0);
m_lay->setSpacing(0);
auto *lay = new QVBoxLayout(m_container);
lay->setContentsMargins(0, 0, 0, 0);
lay->addWidget(m_transitionWidget);
lay->addWidget(m_effectStackWidget);
auto *sc = new QScrollArea;
sc->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
sc->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
sc->setFrameStyle(QFrame::NoFrame);
sc->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding));
m_container->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding));
sc->setWidgetResizable(true);
m_lay->addWidget(sc);
sc->setWidget(m_container);
m_transitionWidget->setVisible(false);
m_effectStackWidget->setVisible(false);
updatePalette();
connect(m_effectStackWidget, &EffectStackView::seekToPos, this, &AssetPanel::seekToPos);
connect(m_effectStackWidget, &EffectStackView::reloadEffect, this, &AssetPanel::reloadEffect);
connect(m_transitionWidget, &TransitionStackView::seekToTransPos, this, &AssetPanel::seekToPos);
connect(m_effectStackWidget, &EffectStackView::updateEnabledState, [this]() { m_enableStackButton->setActive(m_effectStackWidget->isStackEnabled()); });
}
void AssetPanel::showTransition(int tid, const std::shared_ptr &transitionModel)
{
Q_UNUSED(tid)
ObjectId id = transitionModel->getOwnerId();
if (m_transitionWidget->stackOwner() == id) {
// already on this effect stack, do nothing
return;
}
clear();
QString transitionId = transitionModel->getAssetId();
m_switchCompoButton->setCurrentIndex(m_switchCompoButton->findData(transitionId));
m_switchAction->setVisible(true);
m_titleAction->setVisible(false);
m_assetTitle->clear();
m_transitionWidget->setVisible(true);
m_timelineButton->setVisible(true);
m_enableStackButton->setVisible(false);
m_transitionWidget->setModel(transitionModel, QSize(), true);
}
void AssetPanel::showEffectStack(const QString &itemName, const std::shared_ptr &effectsModel, QSize frameSize, bool showKeyframes)
{
if (effectsModel == nullptr) {
// Item is not ready
m_splitButton->setVisible(false);
m_enableStackButton->setVisible(false);
clear();
return;
}
ObjectId id = effectsModel->getOwnerId();
if (m_effectStackWidget->stackOwner() == id) {
// already on this effect stack, do nothing
// Disable split effect in case clip was moved
if (id.first == ObjectType::TimelineClip && m_splitButton->isActive()) {
m_splitButton->setActive(false);
processSplitEffect(false);
}
return;
}
clear();
QString title;
bool showSplit = false;
bool enableKeyframes = false;
switch (id.first) {
case ObjectType::TimelineClip:
title = i18n("%1 effects", itemName);
showSplit = true;
enableKeyframes = true;
break;
case ObjectType::TimelineComposition:
title = i18n("%1 parameters", itemName);
enableKeyframes = true;
break;
case ObjectType::TimelineTrack:
title = i18n("Track %1 effects", itemName);
// TODO: track keyframes
// enableKeyframes = true;
break;
case ObjectType::BinClip:
title = i18n("Bin %1 effects", itemName);
showSplit = true;
break;
default:
title = itemName;
break;
}
m_assetTitle->setText(title);
m_titleAction->setVisible(true);
m_splitButton->setVisible(showSplit);
m_enableStackButton->setVisible(id.first != ObjectType::TimelineComposition);
m_enableStackButton->setActive(effectsModel->isStackEnabled());
if (showSplit) {
m_splitButton->setEnabled(effectsModel->rowCount() > 0);
QObject::connect(effectsModel.get(), &EffectStackModel::dataChanged, [&]() {
if (m_effectStackWidget->isEmpty()) {
m_splitButton->setActive(false);
}
m_splitButton->setEnabled(!m_effectStackWidget->isEmpty());
});
}
m_timelineButton->setVisible(enableKeyframes);
m_timelineButton->setActive(showKeyframes);
// Disable built stack until properly implemented
// m_switchBuiltStack->setVisible(true);
m_effectStackWidget->setVisible(true);
m_effectStackWidget->setModel(effectsModel, frameSize);
}
void AssetPanel::clearAssetPanel(int itemId)
{
ObjectId id = m_effectStackWidget->stackOwner();
if (id.first == ObjectType::TimelineClip && id.second == itemId) {
clear();
} else {
id = m_transitionWidget->stackOwner();
if (id.first == ObjectType::TimelineComposition && id.second == itemId) {
clear();
}
}
}
void AssetPanel::clear()
{
if (m_splitButton->isActive()) {
m_splitButton->setActive(false);
processSplitEffect(false);
}
m_switchAction->setVisible(false);
m_transitionWidget->setVisible(false);
m_transitionWidget->unsetModel();
m_effectStackWidget->setVisible(false);
m_splitButton->setVisible(false);
m_timelineButton->setVisible(false);
m_switchBuiltStack->setVisible(false);
m_effectStackWidget->unsetModel();
m_assetTitle->setText(QString());
}
void AssetPanel::updatePalette()
{
QString styleSheet = getStyleSheet();
setStyleSheet(styleSheet);
m_transitionWidget->setStyleSheet(styleSheet);
m_effectStackWidget->setStyleSheet(styleSheet);
}
// static
const QString AssetPanel::getStyleSheet()
{
KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View);
QColor selected_bg = scheme.decoration(KColorScheme::FocusColor).color();
QColor hgh = KColorUtils::mix(QApplication::palette().window().color(), selected_bg, 0.2);
QColor hover_bg = scheme.decoration(KColorScheme::HoverColor).color();
QColor light_bg = scheme.shade(KColorScheme::LightShade);
QColor alt_bg = scheme.background(KColorScheme::NormalBackground).color();
QString stylesheet;
// effect background
stylesheet.append(QStringLiteral("QFrame#decoframe {border-bottom:2px solid "
"palette(mid);background: transparent} QFrame#decoframe[active=\"true\"] {background: %1;}")
.arg(hgh.name()));
// effect in group background
stylesheet.append(
QStringLiteral("QFrame#decoframesub {border-top:1px solid palette(light);} QFrame#decoframesub[active=\"true\"] {background: %1;}").arg(hgh.name()));
// group background
stylesheet.append(QStringLiteral("QFrame#decoframegroup {border:2px solid palette(dark);margin:0px;margin-top:2px;} "));
// effect title bar
stylesheet.append(QStringLiteral("QFrame#frame {margin-bottom:2px;} QFrame#frame[target=\"true\"] "
"{background: palette(highlight);}"));
// group effect title bar
stylesheet.append(QStringLiteral("QFrame#framegroup {background: palette(dark);} "
"QFrame#framegroup[target=\"true\"] {background: palette(highlight);} "));
// draggable effect bar content
stylesheet.append(QStringLiteral("QProgressBar::chunk:horizontal {background: palette(button);border-top-left-radius: 4px;border-bottom-left-radius: 4px;} "
"QProgressBar::chunk:horizontal#dragOnly {background: %1;border-top-left-radius: 4px;border-bottom-left-radius: 4px;} "
"QProgressBar::chunk:horizontal:hover {background: %2;}")
.arg(alt_bg.name(), selected_bg.name()));
// draggable effect bar
stylesheet.append(QStringLiteral("QProgressBar:horizontal {border: 1px solid palette(dark);border-top-left-radius: 4px;border-bottom-left-radius: "
"4px;border-right:0px;background:%3;padding: 0px;text-align:left center} QProgressBar:horizontal:disabled {border: 1px "
"solid palette(button)} QProgressBar:horizontal#dragOnly {background: %3} QProgressBar:horizontal[inTimeline=\"true\"] { "
"border: 1px solid %1;border-right: 0px;background: %2;padding: 0px;text-align:left center } "
"QProgressBar::chunk:horizontal[inTimeline=\"true\"] {background: %1;}")
.arg(hover_bg.name(), light_bg.name(), alt_bg.name()));
// spin box for draggable widget
stylesheet.append(
QStringLiteral("QAbstractSpinBox#dragBox {border: 1px solid palette(dark);border-top-right-radius: 4px;border-bottom-right-radius: "
"4px;padding-right:0px;} QAbstractSpinBox::down-button#dragBox {width:0px;padding:0px;} QAbstractSpinBox:disabled#dragBox {border: 1px "
"solid palette(button);} QAbstractSpinBox::up-button#dragBox {width:0px;padding:0px;} QAbstractSpinBox[inTimeline=\"true\"]#dragBox { "
"border: 1px solid %1;} QAbstractSpinBox:hover#dragBox {border: 1px solid %2;} ")
.arg(hover_bg.name(), selected_bg.name()));
+ // minimal double edit
+ stylesheet.append(
+ QStringLiteral("QAbstractSpinBox#dragMinimal {border: 0px "
+ ";padding-right:0px; background-color:transparent} QAbstractSpinBox::down-button#dragMinimal {width:0px;padding:0px;} QAbstractSpinBox:disabled#dragMinimal {border: 0px;; background-color:transparent "
+ ";} QAbstractSpinBox::up-button#dragMinimal {width:0px;padding:0px;}")
+ );
// group editable labels
stylesheet.append(QStringLiteral("MyEditableLabel { background-color: transparent; color: palette(bright-text); border-radius: 2px;border: 1px solid "
"transparent;} MyEditableLabel:hover {border: 1px solid palette(highlight);} "));
// transparent qcombobox
stylesheet.append(QStringLiteral("QComboBox { background-color: transparent;} "));
return stylesheet;
}
void AssetPanel::processSplitEffect(bool enable)
{
ObjectType id = m_effectStackWidget->stackOwner().first;
if (id == ObjectType::TimelineClip) {
emit doSplitEffect(enable);
} else if (id == ObjectType::BinClip) {
emit doSplitBinEffect(enable);
}
}
void AssetPanel::showKeyframes(bool enable)
{
if (m_transitionWidget->isVisible()) {
pCore->showClipKeyframes(m_transitionWidget->stackOwner(), enable);
} else {
pCore->showClipKeyframes(m_effectStackWidget->stackOwner(), enable);
}
}
ObjectId AssetPanel::effectStackOwner()
{
if (m_transitionWidget->isVisible()) {
return m_transitionWidget->stackOwner();
}
if (!m_effectStackWidget->isVisible()) {
return ObjectId(ObjectType::NoItem, -1);
}
return m_effectStackWidget->stackOwner();
}
bool AssetPanel::addEffect(const QString &effectId)
{
if (!m_effectStackWidget->isVisible()) {
return false;
}
return m_effectStackWidget->addEffect(effectId);
}
void AssetPanel::enableStack(bool enable)
{
if (!m_effectStackWidget->isVisible()) {
return;
}
m_effectStackWidget->enableStack(enable);
}
void AssetPanel::deleteCurrentEffect()
{
if (m_effectStackWidget->isVisible()) {
m_effectStackWidget->removeCurrentEffect();
}
}
diff --git a/src/assets/view/widgets/colorwheel.cpp b/src/assets/view/widgets/colorwheel.cpp
index 673a36b68..6ae96d263 100644
--- a/src/assets/view/widgets/colorwheel.cpp
+++ b/src/assets/view/widgets/colorwheel.cpp
@@ -1,398 +1,586 @@
/*
* Copyright (c) 2013 Meltytech, LLC
* Author: Dan Dennedy
* Some ideas came from Qt-Plus: https://github.com/liuyanghejerry/Qt-Plus
* and Steinar Gunderson's Movit demo app.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "colorwheel.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
#include
#include
NegQColor NegQColor::fromHsvF(qreal h, qreal s, qreal l, qreal a)
{
NegQColor color;
color.qcolor = QColor::fromHsvF(h, s, l < 0 ? -l : l, a);
color.sign_r = l < 0 ? -1 : 1;
color.sign_g = l < 0 ? -1 : 1;
color.sign_b = l < 0 ? -1 : 1;
return color;
}
NegQColor NegQColor::fromRgbF(qreal r, qreal g, qreal b, qreal a)
{
NegQColor color;
color.qcolor = QColor::fromRgbF(r < 0 ? -r : r, g < 0 ? -g : g, b < 0 ? -b : b, a);
color.sign_r = r < 0 ? -1 : 1;
color.sign_g = g < 0 ? -1 : 1;
color.sign_b = b < 0 ? -1 : 1;
return color;
}
-qreal NegQColor::redF()
+qreal NegQColor::redF() const
{
return qcolor.redF() * sign_r;
}
-qreal NegQColor::greenF()
+void NegQColor::setRedF(qreal val)
+{
+ sign_r = val < 0 ? -1 : 1;
+ qcolor.setRedF(val * sign_r);
+}
+
+qreal NegQColor::greenF() const
{
return qcolor.greenF() * sign_g;
}
-qreal NegQColor::blueF()
+void NegQColor::setGreenF(qreal val)
+{
+ sign_g = val < 0 ? -1 : 1;
+ qcolor.setGreenF(val * sign_g);
+}
+
+qreal NegQColor::blueF() const
{
return qcolor.blueF() * sign_b;
}
-qreal NegQColor::valueF()
+void NegQColor::setBlueF(qreal val)
+{
+ sign_b = val < 0 ? -1 : 1;
+ qcolor.setBlueF(val * sign_b);
+}
+
+qreal NegQColor::valueF() const
{
return qcolor.valueF() * sign_g;
}
-int NegQColor::hue()
+void NegQColor::setValueF(qreal val)
+{
+ qcolor = QColor::fromHsvF(hueF(), saturationF(), val < 0 ? -val : val, 1.);
+ sign_r = val < 0 ? -1 : 1;
+ sign_g = val < 0 ? -1 : 1;
+ sign_b = val < 0 ? -1 : 1;
+}
+
+int NegQColor::hue() const
{
return qcolor.hue();
}
-qreal NegQColor::hueF()
+qreal NegQColor::hueF() const
{
return qcolor.hueF();
}
-qreal NegQColor::saturationF()
+qreal NegQColor::saturationF() const
{
return qcolor.saturationF();
}
-ColorWheel::ColorWheel(QString id, QString name, NegQColor color, QWidget *parent)
+WheelContainer::WheelContainer(QString id, QString name, NegQColor color, int unitSize, QWidget *parent)
: QWidget(parent)
, m_id(std::move(id))
, m_isMouseDown(false)
- , m_margin(5)
+ , m_margin(0)
, m_color(std::move(color))
, m_isInWheel(false)
, m_isInSquare(false)
+ , m_unitSize(unitSize)
, m_name(std::move(name))
{
- QFontInfo info(font());
- m_unitSize = info.pixelSize();
m_initialSize = QSize(m_unitSize * 11, m_unitSize * 11);
m_sliderWidth = m_unitSize * 1.5;
resize(m_initialSize);
setMinimumSize(m_initialSize * .4);
- setMaximumSize(m_initialSize);
+ setMaximumSize(m_initialSize * 1.5);
setCursor(Qt::CrossCursor);
}
-void ColorWheel::setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero)
+
+void WheelContainer::setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero)
{
m_sizeFactor = factor;
m_defaultValue = defvalue;
m_zeroShift = zero;
}
-NegQColor ColorWheel::color() const
+NegQColor WheelContainer::color() const
{
return m_color;
}
-void ColorWheel::setColor(const NegQColor &color)
+void WheelContainer::setColor(QList values)
{
+ const NegQColor color = NegQColor::fromRgbF(values.at(0) / m_sizeFactor, values.at(1) / m_sizeFactor, values.at(2) / m_sizeFactor);
m_color = color;
update();
}
-int ColorWheel::wheelSize() const
+void WheelContainer::setRedColor(double value)
{
- return qMin(width() - m_sliderWidth, height() - m_unitSize);
+ m_color.setRedF(value / m_sizeFactor);
+ emit colorChange(m_color);
+ update();
}
-NegQColor ColorWheel::colorForPoint(const QPointF &point)
+void WheelContainer::setGreenColor(double value)
+{
+ m_color.setGreenF(value / m_sizeFactor);
+ emit colorChange(m_color);
+ update();
+}
+
+void WheelContainer::setBlueColor(double value)
+{
+ m_color.setBlueF(value / m_sizeFactor);
+ emit colorChange(m_color);
+ update();
+}
+
+int WheelContainer::wheelSize() const
+{
+ return qMin(width() - m_sliderWidth, height());
+}
+
+NegQColor WheelContainer::colorForPoint(const QPointF &point)
{
if (!m_image.valid(point.toPoint())) {
return NegQColor();
}
if (m_isInWheel) {
qreal w = wheelSize();
qreal xf = qreal(point.x()) / w;
qreal yf = 1.0 - qreal(point.y()) / w;
qreal xp = 2.0 * xf - 1.0;
qreal yp = 2.0 * yf - 1.0;
qreal rad = qMin(hypot(xp, yp), 1.0);
qreal theta = qAtan2(yp, xp);
theta -= 105.0 / 360.0 * 2.0 * M_PI;
if (theta < 0.0) {
theta += 2.0 * M_PI;
}
qreal hue = (theta * 180.0 / M_PI) / 360.0;
return NegQColor::fromHsvF(hue, rad, m_color.valueF());
}
if (m_isInSquare) {
qreal value = 1.0 - qreal(point.y() - m_margin) / (wheelSize() - m_margin * 2);
if (!qFuzzyCompare(m_zeroShift, 0.)) {
value = value - m_zeroShift;
}
return NegQColor::fromHsvF(m_color.hueF(), m_color.saturationF(), value);
}
return {};
}
-QSize ColorWheel::sizeHint() const
+QSize WheelContainer::sizeHint() const
{
return m_initialSize * .8;
}
-QSize ColorWheel::minimumSizeHint() const
+
+QSize WheelContainer::minimumSizeHint() const
{
return m_initialSize * .4;
}
-void ColorWheel::mousePressEvent(QMouseEvent *event)
+void WheelContainer::wheelEvent(QWheelEvent *event)
+{
+ if (m_sliderRegion.contains(event->pos())) {
+ double y = m_color.valueF();
+ if (event->modifiers() & Qt::ShiftModifier) {
+ y += event->angleDelta().y() > 0 ? 0.002 : -0.002;
+ } else {
+ y += event->angleDelta().y() > 0 ? 0.04 : -0.04;
+ }
+ m_color.setValueF(qBound(-m_zeroShift, y, 1. - m_zeroShift));
+ changeColor(m_color);
+
+ }
+}
+
+void WheelContainer::mousePressEvent(QMouseEvent *event)
{
if (event->modifiers() & Qt::ShiftModifier) {
QPoint clicked = event->pos();
if (m_wheelRegion.contains(clicked)) {
QPointF current = pointForColor();
QPointF diff = clicked - current;
double factor = fabs(diff.x()) > fabs(diff.y()) ? fabs(diff.x()) : fabs(diff.y());
diff /= factor;
m_lastPoint = current + diff;
} else if (m_sliderRegion.contains(clicked)) {
double y = yForColor();
int offset = clicked.y() > y ? 1 : -1;
m_lastPoint = QPointF(clicked.x(), y + offset);
} else {
return;
}
-
} else {
m_lastPoint = event->pos();
}
if (m_wheelRegion.contains(m_lastPoint.toPoint())) {
m_isInWheel = true;
m_isInSquare = false;
if (event->button() == Qt::LeftButton) {
changeColor(colorForPoint(m_lastPoint));
} else {
// reset to default on middle/right button
qreal r = m_color.redF();
qreal b = m_color.blueF();
qreal g = m_color.greenF();
qreal max = qMax(r, b);
max = qMax(max, g);
changeColor(NegQColor::fromRgbF(max, max, max));
}
} else if (m_sliderRegion.contains(m_lastPoint.toPoint())) {
m_isInWheel = false;
m_isInSquare = true;
if (event->button() == Qt::LeftButton) {
changeColor(colorForPoint(m_lastPoint));
} else {
NegQColor c;
c = NegQColor::fromHsvF(m_color.hueF(), m_color.saturationF(), m_defaultValue / m_sizeFactor);
changeColor(c);
}
}
m_isMouseDown = true;
}
-void ColorWheel::mouseMoveEvent(QMouseEvent *event)
+void WheelContainer::mouseMoveEvent(QMouseEvent *event)
{
if (!m_isMouseDown) {
return;
}
if (event->modifiers() & Qt::ShiftModifier) {
if (m_isInWheel) {
QPointF diff = event->pos() - m_lastPoint;
double factor = fabs(diff.x()) > fabs(diff.y()) ? fabs(diff.x()) : fabs(diff.y());
diff /= factor;
m_lastPoint += diff;
} else if (m_isInSquare) {
double y = yForColor();
int offset = event->pos().y() > y ? 1 : -1;
m_lastPoint = QPointF(event->pos().x(), y + offset);
} else {
return;
}
} else {
m_lastPoint = event->pos();
}
if (m_wheelRegion.contains(m_lastPoint.toPoint()) && m_isInWheel) {
const NegQColor color = colorForPoint(m_lastPoint);
changeColor(color);
} else if (m_sliderRegion.contains(m_lastPoint.toPoint()) && m_isInSquare) {
const NegQColor color = colorForPoint(m_lastPoint);
changeColor(color);
}
}
-void ColorWheel::mouseReleaseEvent(QMouseEvent *event)
+void WheelContainer::mouseReleaseEvent(QMouseEvent *event)
{
Q_UNUSED(event)
m_isMouseDown = false;
m_isInWheel = false;
m_isInSquare = false;
}
-void ColorWheel::resizeEvent(QResizeEvent *event)
+void WheelContainer::resizeEvent(QResizeEvent *event)
{
m_image = QImage(event->size(), QImage::Format_ARGB32_Premultiplied);
m_image.fill(palette().window().color().rgb());
drawWheel();
drawSlider();
update();
}
-QString ColorWheel::getParamValues()
+const QString WheelContainer::getParamValues() const
+{
+ return QString::number(m_color.redF() * m_sizeFactor, 'g', 3) + QLatin1Char(',') + QString::number(m_color.greenF() * m_sizeFactor, 'g', 3) +
+ QLatin1Char(',') + QString::number(m_color.blueF() * m_sizeFactor, 'g', 3);
+}
+
+const QList WheelContainer::getNiceParamValues() const
{
- return QString::number(m_color.redF() * m_sizeFactor, 'g', 2) + QLatin1Char(',') + QString::number(m_color.greenF() * m_sizeFactor, 'g', 2) +
- QLatin1Char(',') + QString::number(m_color.blueF() * m_sizeFactor, 'g', 2);
+ return {m_color.redF()* m_sizeFactor, m_color.greenF()* m_sizeFactor , m_color.blueF()* m_sizeFactor};
}
-void ColorWheel::paintEvent(QPaintEvent *event)
+void WheelContainer::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
// QStyleOption opt;
// opt.init(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.drawImage(0, 0, m_image);
// painter.drawRect(0, 0, width(), height());
- painter.drawText(m_margin, wheelSize() + m_unitSize - m_margin, m_name + QLatin1Char(' ') + getParamValues());
+ //painter.drawText(m_margin, wheelSize() + m_unitSize - m_margin, m_name + QLatin1Char(' ') + getParamValues());
drawWheelDot(painter);
drawSliderBar(painter);
// style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
}
-void ColorWheel::drawWheel()
+void WheelContainer::drawWheel()
{
int r = wheelSize();
QPainter painter(&m_image);
painter.setRenderHint(QPainter::Antialiasing);
m_image.fill(0); // transparent
QConicalGradient conicalGradient;
conicalGradient.setColorAt(0.0, Qt::red);
conicalGradient.setColorAt(60.0 / 360.0, Qt::yellow);
conicalGradient.setColorAt(135.0 / 360.0, Qt::green);
conicalGradient.setColorAt(180.0 / 360.0, Qt::cyan);
conicalGradient.setColorAt(240.0 / 360.0, Qt::blue);
conicalGradient.setColorAt(315.0 / 360.0, Qt::magenta);
conicalGradient.setColorAt(1.0, Qt::red);
QRadialGradient radialGradient(0.0, 0.0, r / 2);
radialGradient.setColorAt(0.0, Qt::white);
radialGradient.setColorAt(1.0, Qt::transparent);
painter.translate(r / 2, r / 2);
painter.rotate(-105);
QBrush hueBrush(conicalGradient);
painter.setPen(Qt::NoPen);
painter.setBrush(hueBrush);
- painter.drawEllipse(QPoint(0, 0), r / 2 - m_margin, r / 2 - m_margin);
+ painter.drawEllipse(QPointF(0, 0), r / 2 - m_margin, r / 2 - m_margin);
QBrush saturationBrush(radialGradient);
painter.setBrush(saturationBrush);
- painter.drawEllipse(QPoint(0, 0), r / 2 - m_margin, r / 2 - m_margin);
+ painter.drawEllipse(QPointF(0, 0), r / 2 - m_margin, r / 2 - m_margin);
+
+ painter.setBrush(Qt::gray);
+ painter.setOpacity(0.4);
+ painter.drawEllipse(QPointF(0, 0), r / 2 - m_unitSize * .6, r / 2 - m_unitSize * .6);
m_wheelRegion = QRegion(r / 2, r / 2, r - 2 * m_margin, r - 2 * m_margin, QRegion::Ellipse);
m_wheelRegion.translate(-(r - 2 * m_margin) / 2, -(r - 2 * m_margin) / 2);
}
-void ColorWheel::drawSlider()
+void WheelContainer::drawSlider()
{
QPainter painter(&m_image);
painter.setRenderHint(QPainter::Antialiasing);
- int ws = wheelSize();
+ int ws = wheelSize() + m_unitSize * .2;
qreal scale = qreal(ws + m_sliderWidth) / maximumWidth();
- int w = m_sliderWidth * scale;
+ int w = m_sliderWidth * scale - m_unitSize * .4;
int h = ws - m_margin * 2;
QLinearGradient gradient(0, 0, w, h);
gradient.setColorAt(0.0, Qt::white);
gradient.setColorAt(1.0, Qt::black);
QBrush brush(gradient);
painter.setPen(Qt::NoPen);
painter.setBrush(brush);
painter.translate(ws, m_margin);
painter.drawRect(0, 0, w, h);
m_sliderRegion = QRegion(ws, m_margin, w, h);
}
-void ColorWheel::drawWheelDot(QPainter &painter)
+void WheelContainer::drawWheelDot(QPainter &painter)
{
int r = wheelSize() / 2;
QPen pen(Qt::white);
pen.setWidth(2);
painter.setPen(pen);
painter.setBrush(Qt::black);
painter.translate(r, r);
painter.rotate(360.0 - m_color.hue());
painter.rotate(-105);
// r -= margin;
painter.drawEllipse(QPointF(m_color.saturationF() * r, 0.0), 4, 4);
painter.resetTransform();
}
-QPointF ColorWheel::pointForColor()
+QPointF WheelContainer::pointForColor()
{
int r = wheelSize() / 2;
QTransform transform;
transform.translate(r, r);
transform.rotate(255 - m_color.hue());
transform.translate(m_color.saturationF() * r, 0);
return transform.map(QPointF(0, 0));
}
-double ColorWheel::yForColor()
+double WheelContainer::yForColor()
{
qreal value = 1.0 - m_color.valueF();
if (m_id == QLatin1String("lift")) {
value -= m_zeroShift;
}
int ws = wheelSize();
int h = ws - m_margin * 2;
return m_margin + value * h;
}
-void ColorWheel::drawSliderBar(QPainter &painter)
+void WheelContainer::drawSliderBar(QPainter &painter)
{
qreal value = 1.0 - m_color.valueF();
if (m_id == QLatin1String("lift")) {
value -= m_zeroShift;
}
int ws = wheelSize();
qreal scale = qreal(ws + m_sliderWidth) / maximumWidth();
int w = m_sliderWidth * scale;
int h = ws - m_margin * 2;
QPen pen(Qt::white);
pen.setWidth(2);
painter.setPen(pen);
painter.setBrush(Qt::black);
painter.translate(ws, m_margin + value * h);
painter.drawRect(0, 0, w, 4);
painter.resetTransform();
}
-void ColorWheel::changeColor(const NegQColor &color)
+void WheelContainer::changeColor(const NegQColor &color)
{
m_color = color;
drawWheel();
drawSlider();
update();
emit colorChange(m_color);
}
+
+
+ColorWheel::ColorWheel(QString id, QString name, NegQColor color, QWidget *parent)
+ : QWidget(parent)
+{
+ QFontInfo info(font());
+ int unitSize = info.pixelSize();
+ QVBoxLayout *lay = new QVBoxLayout(this);
+ m_wheelName = new QLabel(name, this);
+ m_wheelName->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
+ lay->addWidget(m_wheelName);
+ m_container = new WheelContainer(id, name, color, unitSize, this);
+ QHBoxLayout *hb = new QHBoxLayout;
+ m_redEdit = new QDoubleSpinBox(this);
+ m_redEdit->setPrefix(i18n("R: "));
+ m_redEdit->setFrame(QFrame::NoFrame);
+ m_redEdit->setDecimals(3);
+ m_redEdit->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ m_redEdit->setFont(m_wheelName->font());
+ m_redEdit->setObjectName(QStringLiteral("dragMinimal"));
+ m_greenEdit = new QDoubleSpinBox(this);
+ m_greenEdit->setPrefix(i18n("G: "));
+ m_greenEdit->setObjectName(QStringLiteral("dragMinimal"));
+ m_greenEdit->setFont(m_wheelName->font());
+ m_greenEdit->setDecimals(3);
+ m_greenEdit->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ m_blueEdit = new QDoubleSpinBox(this);
+ m_blueEdit->setPrefix(i18n("B: "));
+ m_blueEdit->setObjectName(QStringLiteral("dragMinimal"));
+ m_blueEdit->setFont(m_wheelName->font());
+ m_blueEdit->setDecimals(3);
+ m_blueEdit->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ lay->setContentsMargins(0, 0, 2, 0);
+ lay->setSpacing(0);
+ lay->addWidget(m_container);
+ hb->addWidget(m_redEdit);
+ hb->addWidget(m_greenEdit);
+ hb->addWidget(m_blueEdit);
+ hb->setSpacing(0);
+ hb->setContentsMargins(0, 0, 0, 0);
+ lay->addLayout(hb);
+ m_container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+ connect(m_container, &WheelContainer::colorChange, [&] (const NegQColor &col) {
+ QList vals = m_container->getNiceParamValues();
+ m_redEdit->blockSignals(true);
+ m_greenEdit->blockSignals(true);
+ m_blueEdit->blockSignals(true);
+ m_redEdit->setValue(vals.at(0));
+ m_greenEdit->setValue(vals.at(1));
+ m_blueEdit->setValue(vals.at(2));
+ m_redEdit->blockSignals(false);
+ m_greenEdit->blockSignals(false);
+ m_blueEdit->blockSignals(false);
+ emit colorChange(col);
+ });
+ connect(m_redEdit, static_cast(&QDoubleSpinBox::valueChanged), [&]() {
+ m_container->setRedColor(m_redEdit->value());
+ });
+ connect(m_greenEdit, static_cast(&QDoubleSpinBox::valueChanged), [&]() {
+ m_container->setGreenColor(m_greenEdit->value());
+ });
+ connect(m_blueEdit, static_cast(&QDoubleSpinBox::valueChanged), [&]() {
+ m_container->setBlueColor(m_blueEdit->value());
+ });
+ setMinimumHeight(m_wheelName->height() + m_container->minimumHeight() + m_redEdit->height());
+ setMaximumWidth(m_container->maximumWidth());
+ setMinimumWidth(3 * m_redEdit->sizeHint().width());
+}
+
+
+NegQColor ColorWheel::color() const
+{
+ return m_container->color();
+}
+
+void ColorWheel::setColor(QList values)
+{
+ m_container->setColor(values);
+ QList vals = m_container->getNiceParamValues();
+ m_redEdit->blockSignals(true);
+ m_greenEdit->blockSignals(true);
+ m_blueEdit->blockSignals(true);
+ m_redEdit->setValue(values.at(0));
+ m_greenEdit->setValue(values.at(1));
+ m_blueEdit->setValue(values.at(2));
+ m_redEdit->blockSignals(false);
+ m_greenEdit->blockSignals(false);
+ m_blueEdit->blockSignals(false);
+}
+
+void ColorWheel::setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero)
+{
+ m_container->setFactorDefaultZero(factor, defvalue, zero);
+ if (zero > 0) {
+ // Lift has a special range
+ m_redEdit->setRange(-1, 1);
+ m_greenEdit->setRange(-1, 1);
+ m_blueEdit->setRange(-1, 1);
+ } else {
+ m_redEdit->setRange(0, factor);
+ m_greenEdit->setRange(0, factor);
+ m_blueEdit->setRange(0, factor);
+ }
+ m_redEdit->setSingleStep(.01);
+ m_greenEdit->setSingleStep(.01);
+ m_blueEdit->setSingleStep(.01);
+}
+
diff --git a/src/assets/view/widgets/colorwheel.h b/src/assets/view/widgets/colorwheel.h
index 190bca26e..85360eea1 100644
--- a/src/assets/view/widgets/colorwheel.h
+++ b/src/assets/view/widgets/colorwheel.h
@@ -1,100 +1,131 @@
/*
* Copyright (c) 2013 Meltytech, LLC
* Author: Dan Dennedy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifndef COLORWHEELPARAM_H
#define COLORWHEELPARAM_H
#include
#include
#include
+class QDoubleSpinBox;
+class QLabel;
+
class NegQColor
{
public:
int8_t sign_r = 1;
int8_t sign_g = 1;
int8_t sign_b = 1;
QColor qcolor;
static NegQColor fromHsvF(qreal h, qreal s, qreal l, qreal a = 1.0);
static NegQColor fromRgbF(qreal r, qreal g, qreal b, qreal a = 1.0);
- qreal redF();
- qreal greenF();
- qreal blueF();
- qreal valueF();
- int hue();
- qreal hueF();
- qreal saturationF();
+ qreal redF() const;
+ qreal greenF() const;
+ qreal blueF() const;
+ qreal valueF() const;
+ int hue() const;
+ qreal hueF() const;
+ qreal saturationF() const;
+ void setRedF(qreal val);
+ void setGreenF(qreal val);
+ void setBlueF(qreal val);
+ void setValueF(qreal val);
};
-class ColorWheel : public QWidget
+class WheelContainer : public QWidget
{
Q_OBJECT
public:
- explicit ColorWheel(QString id, QString name, NegQColor color, QWidget *parent = nullptr);
-
+ explicit WheelContainer(QString id, QString name, NegQColor color, int unitSize, QWidget *parent = nullptr);
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
NegQColor color() const;
- void setColor(const NegQColor &color);
+ void setColor(QList values);
void setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero);
-
-signals:
- void colorChange(const NegQColor &color);
+ const QList getNiceParamValues() const;
+ void setRedColor(double value);
+ void setGreenColor(double value);
+ void setBlueColor(double value);
public slots:
void changeColor(const NegQColor &color);
+signals:
+ void colorChange(const NegQColor &color);
+
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
+ void wheelEvent(QWheelEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
private:
QString m_id;
QSize m_initialSize;
QImage m_image;
bool m_isMouseDown;
QPointF m_lastPoint;
int m_margin;
int m_sliderWidth;
QRegion m_wheelRegion;
QRegion m_sliderRegion;
NegQColor m_color;
bool m_isInWheel;
bool m_isInSquare;
int m_unitSize;
QString m_name;
qreal m_sizeFactor = 1;
qreal m_defaultValue = 1;
qreal m_zeroShift = 0;
int wheelSize() const;
NegQColor colorForPoint(const QPointF &point);
QPointF pointForColor();
double yForColor();
void drawWheel();
void drawWheelDot(QPainter &painter);
void drawSliderBar(QPainter &painter);
void drawSlider();
- QString getParamValues();
+ const QString getParamValues() const;
+};
+
+class ColorWheel : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ColorWheel(QString id, QString name, NegQColor color, QWidget *parent = nullptr);
+ NegQColor color() const;
+ void setColor(QList values);
+ void setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero);
+
+private:
+ WheelContainer *m_container;
+ QLabel *m_wheelName;
+ QDoubleSpinBox *m_redEdit;
+ QDoubleSpinBox *m_greenEdit;
+ QDoubleSpinBox *m_blueEdit;
+
+signals:
+ void colorChange(const NegQColor &color);
};
#endif // COLORWHEEL_H
diff --git a/src/assets/view/widgets/lumaliftgainparam.cpp b/src/assets/view/widgets/lumaliftgainparam.cpp
index 367a7b1e0..32bff477a 100644
--- a/src/assets/view/widgets/lumaliftgainparam.cpp
+++ b/src/assets/view/widgets/lumaliftgainparam.cpp
@@ -1,140 +1,132 @@
/***************************************************************************
* Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* Some code was borrowed from shotcut *
* *
* 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 "lumaliftgainparam.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "colorwheel.h"
#include "utils/flowlayout.h"
#include
static const double LIFT_FACTOR = 2.0;
static const double GAMMA_FACTOR = 2.0;
static const double GAIN_FACTOR = 4.0;
LumaLiftGainParam::LumaLiftGainParam(std::shared_ptr model, QModelIndex index, QWidget *parent)
: AbstractParamWidget(std::move(model), index, parent)
{
m_flowLayout = new FlowLayout(this, 2, 2, 2);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
m_locale.setNumberOptions(QLocale::OmitGroupSeparator);
m_lift = new ColorWheel(QStringLiteral("lift"), i18n("Lift"), NegQColor(), this);
m_lift->setFactorDefaultZero(LIFT_FACTOR, 0, 0.5);
connect(m_lift, &ColorWheel::colorChange, this, &LumaLiftGainParam::liftChanged);
m_gamma = new ColorWheel(QStringLiteral("gamma"), i18n("Gamma"), NegQColor(), this);
m_gamma->setFactorDefaultZero(GAMMA_FACTOR, 1, 0);
connect(m_gamma, &ColorWheel::colorChange, this, &LumaLiftGainParam::gammaChanged);
m_gain = new ColorWheel(QStringLiteral("gain"), i18n("Gain"), NegQColor(), this);
m_gain->setFactorDefaultZero(GAIN_FACTOR, 1, 0);
connect(m_gain, &ColorWheel::colorChange, this, &LumaLiftGainParam::gainChanged);
QMap indexes;
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex local_index = m_model->index(i, 0);
QString name = m_model->data(local_index, AssetParameterModel::NameRole).toString();
indexes.insert(name, local_index);
}
m_flowLayout->addWidget(m_lift);
m_flowLayout->addWidget(m_gamma);
m_flowLayout->addWidget(m_gain);
setLayout(m_flowLayout);
slotRefresh();
connect(this, &LumaLiftGainParam::liftChanged, [this, indexes]() {
NegQColor liftColor = m_lift->color();
QList ixes{indexes.value(QStringLiteral("lift_r")),indexes.value(QStringLiteral("lift_g")), indexes.value(QStringLiteral("lift_b"))};
QStringList values {m_locale.toString(liftColor.redF()), m_locale.toString(liftColor.greenF()), m_locale.toString(liftColor.blueF())};
emit valuesChanged(ixes, values, true);
});
connect(this, &LumaLiftGainParam::gammaChanged, [this, indexes]() {
NegQColor gammaColor = m_gamma->color();
QList ixes{indexes.value(QStringLiteral("gamma_r")),indexes.value(QStringLiteral("gamma_g")), indexes.value(QStringLiteral("gamma_b"))};
QStringList values {m_locale.toString(gammaColor.redF() * GAMMA_FACTOR), m_locale.toString(gammaColor.greenF() * GAMMA_FACTOR), m_locale.toString(gammaColor.blueF() * GAMMA_FACTOR)};
emit valuesChanged(ixes, values, true);
});
connect(this, &LumaLiftGainParam::gainChanged, [this, indexes]() {
NegQColor gainColor = m_gain->color();
QList ixes{indexes.value(QStringLiteral("gain_r")),indexes.value(QStringLiteral("gain_g")), indexes.value(QStringLiteral("gain_b"))};
QStringList values {m_locale.toString(gainColor.redF() * GAIN_FACTOR), m_locale.toString(gainColor.greenF() * GAIN_FACTOR), m_locale.toString(gainColor.blueF() * GAIN_FACTOR)};
emit valuesChanged(ixes, values, true);
});
}
void LumaLiftGainParam::updateEffect(QDomElement &effect)
{
NegQColor lift = m_lift->color();
NegQColor gamma = m_gamma->color();
NegQColor gain = m_gain->color();
QMap values;
values.insert(QStringLiteral("lift_r"), lift.redF() * LIFT_FACTOR);
values.insert(QStringLiteral("lift_g"), lift.greenF() * LIFT_FACTOR);
values.insert(QStringLiteral("lift_b"), lift.blueF() * LIFT_FACTOR);
values.insert(QStringLiteral("gamma_r"), gamma.redF() * GAMMA_FACTOR);
values.insert(QStringLiteral("gamma_g"), gamma.greenF() * GAMMA_FACTOR);
values.insert(QStringLiteral("gamma_b"), gamma.blueF() * GAMMA_FACTOR);
values.insert(QStringLiteral("gain_r"), gain.redF() * GAIN_FACTOR);
values.insert(QStringLiteral("gain_g"), gain.greenF() * GAIN_FACTOR);
values.insert(QStringLiteral("gain_b"), gain.blueF() * GAIN_FACTOR);
QDomNodeList namenode = effect.childNodes();
for (int i = 0; i < namenode.count(); ++i) {
QDomElement pa = namenode.item(i).toElement();
if (pa.tagName() != QLatin1String("parameter")) {
continue;
}
if (values.contains(pa.attribute(QStringLiteral("name")))) {
pa.setAttribute(QStringLiteral("value"), (int)(values.value(pa.attribute(QStringLiteral("name"))) *
m_locale.toDouble(pa.attribute(QStringLiteral("factor"), QStringLiteral("1")))));
}
}
}
void LumaLiftGainParam::resizeEvent(QResizeEvent *ev)
{
setFixedHeight(m_flowLayout->miniHeight());
QWidget::resizeEvent(ev);
emit updateHeight();
}
void LumaLiftGainParam::slotShowComment(bool) {}
void LumaLiftGainParam::slotRefresh()
{
QMap values;
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex local_index = m_model->index(i, 0);
QString name = m_model->data(local_index, AssetParameterModel::NameRole).toString();
double val = m_locale.toDouble(m_model->data(local_index, AssetParameterModel::ValueRole).toString());
values.insert(name, val);
}
-
- NegQColor lift = NegQColor::fromRgbF(values.value(QStringLiteral("lift_r")) / LIFT_FACTOR, values.value(QStringLiteral("lift_g")) / LIFT_FACTOR,
- values.value(QStringLiteral("lift_b")) / LIFT_FACTOR);
- NegQColor gamma = NegQColor::fromRgbF(values.value(QStringLiteral("gamma_r")) / GAMMA_FACTOR, values.value(QStringLiteral("gamma_g")) / GAMMA_FACTOR,
- values.value(QStringLiteral("gamma_b")) / GAMMA_FACTOR);
- NegQColor gain = NegQColor::fromRgbF(values.value(QStringLiteral("gain_r")) / GAIN_FACTOR, values.value(QStringLiteral("gain_g")) / GAIN_FACTOR,
- values.value(QStringLiteral("gain_b")) / GAIN_FACTOR);
-
- m_lift->setColor(lift);
- m_gamma->setColor(gamma);
- m_gain->setColor(gain);
+ m_lift->setColor({values.value(QStringLiteral("lift_r")), values.value(QStringLiteral("lift_g")), values.value(QStringLiteral("lift_b"))});
+ m_gamma->setColor({values.value(QStringLiteral("gamma_r")), values.value(QStringLiteral("gamma_g")), values.value(QStringLiteral("gamma_b"))});
+ m_gain->setColor({values.value(QStringLiteral("gain_r")), values.value(QStringLiteral("gain_g")), values.value(QStringLiteral("gain_b"))});
}
diff --git a/src/core.cpp b/src/core.cpp
index fad810c39..38867b71c 100644
--- a/src/core.cpp
+++ b/src/core.cpp
@@ -1,778 +1,792 @@
/*
Copyright (C) 2014 Till Theato
This file is part of kdenlive. See www.kdenlive.org.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
*/
#include "core.h"
#include "bin/bin.h"
#include "bin/projectitemmodel.h"
#include "capture/mediacapture.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "jobs/jobmanager.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
#include "library/librarywidget.h"
#include "audiomixer/mixermanager.hpp"
#include "mainwindow.h"
#include "mltconnection.h"
#include "mltcontroller/clipcontroller.h"
#include "monitor/monitormanager.h"
#include "profiles/profilemodel.hpp"
#include "profiles/profilerepository.hpp"
#include "project/projectmanager.h"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/view/timelinecontroller.h"
#include "timeline2/view/timelinewidget.h"
#include
#include
#include
#include
#include
#include
#ifdef Q_OS_MAC
#include
#endif
std::unique_ptr Core::m_self;
Core::Core()
: m_thumbProfile(nullptr)
, m_capture(new MediaCapture(this))
{
}
void Core::prepareShutdown()
{
m_guiConstructed = false;
QThreadPool::globalInstance()->clear();
}
Core::~Core()
{
qDebug() << "deleting core";
if (m_monitorManager) {
delete m_monitorManager;
}
// delete m_binWidget;
if (m_projectManager) {
delete m_projectManager;
}
ClipController::mediaUnavailable.reset();
}
void Core::build(bool isAppImage, const QString &MltPath)
{
if (m_self) {
return;
}
m_self.reset(new Core());
m_self->initLocale();
qRegisterMetaType("audioShortVector");
qRegisterMetaType>("QVector");
qRegisterMetaType("MessageType");
qRegisterMetaType("stringMap");
qRegisterMetaType("audioByteArray");
qRegisterMetaType>("QList");
qRegisterMetaType>("std::shared_ptr");
qRegisterMetaType>();
qRegisterMetaType("QDomElement");
qRegisterMetaType("requestClipInfo");
if (isAppImage) {
QString appPath = qApp->applicationDirPath();
KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg")));
KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay")));
KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe")));
KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt")));
MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles")));
} else {
// Open connection with Mlt
MltConnection::construct(MltPath);
}
// load the profile from disk
ProfileRepository::get()->refresh();
// load default profile
m_self->m_profile = KdenliveSettings::default_profile();
if (m_self->m_profile.isEmpty()) {
m_self->m_profile = ProjectManager::getDefaultProjectFormat();
KdenliveSettings::setDefault_profile(m_self->m_profile);
}
// Init producer shown for unavailable media
// TODO make it a more proper image, it currently causes a crash on exit
ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue");
ClipController::mediaUnavailable->set("length", 99999999);
m_self->m_projectItemModel = ProjectItemModel::construct();
// Job manager must be created before bin to correctly connect
m_self->m_jobManager.reset(new JobManager(m_self.get()));
}
void Core::initGUI(const QUrl &Url)
{
m_guiConstructed = true;
m_profile = KdenliveSettings::default_profile();
m_currentProfile = m_profile;
profileChanged();
m_mainWindow = new MainWindow();
connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences);
// load default profile and ask user to select one if not found.
if (m_profile.isEmpty()) {
m_profile = ProjectManager::getDefaultProjectFormat();
profileChanged();
KdenliveSettings::setDefault_profile(m_profile);
}
if (!ProfileRepository::get()->profileExists(m_profile)) {
KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value."));
// TODO this simple widget should be improved and probably use profileWidget
// we get the list of profiles
QVector> all_profiles = ProfileRepository::get()->getAllProfiles();
QStringList all_descriptions;
for (const auto &profile : all_profiles) {
all_descriptions << profile.first;
}
// ask the user
bool ok;
QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok);
if (ok) {
ok = false;
for (const auto &profile : all_profiles) {
if (profile.first == item) {
m_profile = profile.second;
ok = true;
}
}
}
if (!ok) {
KMessageBox::error(
m_mainWindow,
i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel"));
m_profile = QStringLiteral("dv_pal");
}
KdenliveSettings::setDefault_profile(m_profile);
profileChanged();
}
m_projectManager = new ProjectManager(this);
m_binWidget = new Bin(m_projectItemModel, m_mainWindow);
m_library = new LibraryWidget(m_projectManager, m_mainWindow);
m_mixerWidget = new MixerManager(m_mainWindow);
connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList)));
connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath);
connect(m_capture.get(), &MediaCapture::recordStateChanged, m_mixerWidget, &MixerManager::recordStateChanged);
m_monitorManager = new MonitorManager(this);
connect(m_monitorManager, &MonitorManager::cleanMixer, m_mixerWidget, &MixerManager::resetAudioValues);
// Producer queue, creating MLT::Producers on request
/*
m_producerQueue = new ProducerQueue(m_binController);
connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady);
connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady);
connect(m_producerQueue, &ProducerQueue::requestProxy,
[this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);});
connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection);
connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap)));
connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int)));
connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection);
// TODO
connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/
m_mainWindow->init();
projectManager()->init(Url, QString());
if (qApp->isSessionRestored()) {
// NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow
m_mainWindow->restore(1, false);
}
QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection);
m_mainWindow->show();
QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount()));
}
std::unique_ptr &Core::self()
{
if (!m_self) {
qDebug() << "Error : Core has not been created";
}
return m_self;
}
MainWindow *Core::window()
{
return m_mainWindow;
}
ProjectManager *Core::projectManager()
{
return m_projectManager;
}
MonitorManager *Core::monitorManager()
{
return m_monitorManager;
}
Monitor *Core::getMonitor(int id)
{
if (id == Kdenlive::ClipMonitor) {
return m_monitorManager->clipMonitor();
}
return m_monitorManager->projectMonitor();
}
Bin *Core::bin()
{
return m_binWidget;
}
void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone)
{
m_binWidget->selectClipById(clipId, frame, zone);
}
std::shared_ptr Core::jobManager()
{
return m_jobManager;
}
LibraryWidget *Core::library()
{
return m_library;
}
MixerManager *Core::mixer()
{
return m_mixerWidget;
}
void Core::initLocale()
{
QLocale systemLocale = QLocale();
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, nullptr);
#else
setlocale(LC_NUMERIC_MASK, nullptr);
#endif
// localeconv()->decimal_point does not give reliable results on Windows
#ifndef Q_OS_WIN
char *separator = localeconv()->decimal_point;
if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) {
// qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------";
// HACK: There is a locale conflict, so set locale to C
// Make sure to override exported values or it won't work
qputenv("LANG", "C");
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, "C");
#else
setlocale(LC_NUMERIC_MASK, "C");
#endif
systemLocale = QLocale::c();
}
#endif
systemLocale.setNumberOptions(QLocale::OmitGroupSeparator);
QLocale::setDefault(systemLocale);
}
std::unique_ptr &Core::getMltRepository()
{
return MltConnection::self()->getMltRepository();
}
std::unique_ptr &Core::getCurrentProfile() const
{
return ProfileRepository::get()->getProfile(m_currentProfile);
}
const QString &Core::getCurrentProfilePath() const
{
return m_currentProfile;
}
bool Core::setCurrentProfile(const QString &profilePath)
{
if (m_currentProfile == profilePath) {
// no change required
return true;
}
if (ProfileRepository::get()->profileExists(profilePath)) {
m_currentProfile = profilePath;
m_thumbProfile.reset();
// inform render widget
profileChanged();
m_mainWindow->updateRenderWidgetProfile();
if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) {
m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile());
checkProfileValidity();
}
return true;
}
return false;
}
void Core::checkProfileValidity()
{
int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2);
if (offset > 0) {
// Profile is broken, warn user
if (m_binWidget) {
m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning);
}
}
}
double Core::getCurrentSar() const
{
return getCurrentProfile()->sar();
}
double Core::getCurrentDar() const
{
return getCurrentProfile()->dar();
}
double Core::getCurrentFps() const
{
return getCurrentProfile()->fps();
}
QSize Core::getCurrentFrameDisplaySize() const
{
return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()};
}
QSize Core::getCurrentFrameSize() const
{
return {getCurrentProfile()->width(), getCurrentProfile()->height()};
}
void Core::requestMonitorRefresh()
{
if (!m_guiConstructed) return;
m_monitorManager->refreshProjectMonitor();
}
void Core::refreshProjectRange(QSize range)
{
if (!m_guiConstructed) return;
m_monitorManager->refreshProjectRange(range);
}
int Core::getItemPosition(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second);
}
break;
case ObjectType::BinClip:
case ObjectType::TimelineTrack:
+ case ObjectType::Master:
return 0;
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
int Core::getItemIn(const ObjectId &id)
{
if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) {
qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!";
return 0;
}
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second);
} else {
qDebug()<<"// ERROR QUERYING NON CLIP PROPERTIES\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
}
break;
case ObjectType::TimelineComposition:
case ObjectType::BinClip:
case ObjectType::TimelineTrack:
+ case ObjectType::Master:
return 0;
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
PlaylistState::ClipState Core::getItemState(const ObjectId &id)
{
if (!m_guiConstructed) return PlaylistState::Disabled;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second);
}
break;
case ObjectType::TimelineComposition:
return PlaylistState::VideoOnly;
break;
case ObjectType::BinClip:
return m_binWidget->getClipState(id.second);
break;
case ObjectType::TimelineTrack:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly;
+ case ObjectType::Master:
+ return PlaylistState::Disabled;
+ break;
default:
qDebug() << "ERROR: unhandled object type";
break;
}
return PlaylistState::Disabled;
}
int Core::getItemDuration(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second);
}
break;
case ObjectType::BinClip:
return (int)m_binWidget->getClipDuration(id.second);
break;
case ObjectType::TimelineTrack:
+ case ObjectType::Master:
return m_mainWindow->getCurrentTimeline()->controller()->duration();
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
int Core::getItemTrack(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
case ObjectType::TimelineComposition:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second);
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
void Core::refreshProjectItem(const ObjectId &id)
{
if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second);
}
break;
case ObjectType::TimelineTrack:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) {
requestMonitorRefresh();
}
break;
case ObjectType::BinClip:
m_monitorManager->refreshClipMonitor();
break;
+ case ObjectType::Master:
+ requestMonitorRefresh();
+ break;
default:
qDebug() << "ERROR: unhandled object type";
}
}
bool Core::hasTimelinePreview() const
{
if (!m_guiConstructed) {
return false;
}
return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0;
}
KdenliveDoc *Core::currentDoc()
{
return m_projectManager->current();
}
int Core::projectDuration() const
{
if (!m_guiConstructed) {
return 0;
}
return m_mainWindow->getCurrentTimeline()->controller()->duration();
}
void Core::profileChanged()
{
GenTime::setFps(getCurrentFps());
}
void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text)
{
undoStack()->push(new FunctionalUndoCommand(undo, redo, text));
}
void Core::pushUndo(QUndoCommand *command)
{
undoStack()->push(command);
}
void Core::displayMessage(const QString &message, MessageType type, int timeout)
{
if (m_mainWindow) {
m_mainWindow->displayMessage(message, type, timeout);
} else {
qDebug() << message;
}
}
void Core::displayBinMessage(const QString &text, int type, const QList &actions)
{
m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions);
}
void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo)
{
m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo);
}
void Core::clearAssetPanel(int itemId)
{
if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId);
}
std::shared_ptr Core::getItemEffectStack(int itemType, int itemId)
{
if (!m_guiConstructed) return nullptr;
switch (itemType) {
case (int)ObjectType::TimelineClip:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId);
case (int)ObjectType::TimelineTrack:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId);
break;
case (int)ObjectType::BinClip:
return m_binWidget->getClipEffectStack(itemId);
+ case (int)ObjectType::Master:
+ return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getMasterEffectStackModel();
default:
return nullptr;
}
}
std::shared_ptr Core::undoStack()
{
return projectManager()->undoStack();
}
QMap Core::getTrackNames(bool videoOnly)
{
if (!m_guiConstructed) return QMap();
return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(videoOnly);
}
QPair Core::getCompositionATrack(int cid) const
{
if (!m_guiConstructed) return {};
return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid);
}
bool Core::compositionAutoTrack(int cid) const
{
return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid);
}
void Core::setCompositionATrack(int cid, int aTrack)
{
if (!m_guiConstructed) return;
m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack);
}
std::shared_ptr Core::projectItemModel()
{
return m_projectItemModel;
}
void Core::invalidateRange(QSize range)
{
if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return;
m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height());
}
void Core::invalidateItem(ObjectId itemId)
{
if (!m_mainWindow || !m_mainWindow->getCurrentTimeline() || m_mainWindow->getCurrentTimeline()->loading) return;
switch (itemId.first) {
case ObjectType::TimelineClip:
case ObjectType::TimelineComposition:
m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second);
break;
case ObjectType::TimelineTrack:
m_mainWindow->getCurrentTimeline()->controller()->invalidateTrack(itemId.second);
break;
case ObjectType::BinClip:
m_binWidget->invalidateClip(QString::number(itemId.second));
break;
+ case ObjectType::Master:
+ m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(0, -1);
+ break;
default:
// compositions should not have effects
break;
}
}
double Core::getClipSpeed(int id) const
{
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id);
}
void Core::updateItemKeyframes(ObjectId id)
{
if (id.first == ObjectType::TimelineClip && m_mainWindow) {
m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole});
}
}
void Core::updateItemModel(ObjectId id, const QString &service)
{
if (m_mainWindow && id.first == ObjectType::TimelineClip && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade"))) {
bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black");
m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole});
}
}
void Core::showClipKeyframes(ObjectId id, bool enable)
{
if (id.first == ObjectType::TimelineClip) {
m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable);
} else if (id.first == ObjectType::TimelineComposition) {
m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable);
}
}
Mlt::Profile *Core::thumbProfile()
{
QMutexLocker lck(&m_thumbProfileMutex);
if (!m_thumbProfile) {
m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str());
m_thumbProfile->set_height(144);
int width = 144 * m_thumbProfile->dar() + 0.5;
if (width % 8 > 0) {
width += 8 - width % 8;
}
m_thumbProfile->set_width(width);
}
return m_thumbProfile.get();
}
int Core::getTimelinePosition() const
{
if (m_mainWindow && m_guiConstructed) {
return m_mainWindow->getCurrentTimeline()->controller()->timelinePosition();
}
return 0;
}
void Core::triggerAction(const QString &name)
{
QAction *action = m_mainWindow->actionCollection()->action(name);
if (action) {
action->trigger();
}
}
void Core::clean()
{
m_self.reset();
}
void Core::startMediaCapture(int tid, bool checkAudio, bool checkVideo)
{
if (checkAudio && checkVideo) {
m_capture->recordVideo(tid, true);
} else if (checkAudio) {
m_capture->recordAudio(tid, true);
}
m_mediaCaptureFile = m_capture->getCaptureOutputLocation();
}
void Core::stopMediaCapture(int tid, bool checkAudio, bool checkVideo)
{
if (checkAudio && checkVideo) {
m_capture->recordVideo(tid, false);
} else if (checkAudio) {
m_capture->recordAudio(tid, false);
}
}
QStringList Core::getAudioCaptureDevices()
{
return m_capture->getAudioCaptureDevices();
}
int Core::getMediaCaptureState()
{
return m_capture->getState();
}
bool Core::isMediaCapturing()
{
return m_capture->isRecording();
}
MediaCapture *Core::getAudioDevice()
{
return m_capture.get();
}
QString Core::getProjectFolderName()
{
if (currentDoc()) {
return currentDoc()->projectDataFolder() + QDir::separator();
}
return QString();
}
QString Core::getTimelineClipBinId(int cid)
{
if (!m_guiConstructed) {
return QString();
}
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid);
}
return QString();
}
int Core::getDurationFromString(const QString &time)
{
if (!m_guiConstructed) {
return 0;
}
const QString duration = currentDoc()->timecode().reformatSeparators(time);
return currentDoc()->timecode().getFrameCount(duration);
}
diff --git a/src/definitions.h b/src/definitions.h
index a55ec8df7..c82b64efc 100644
--- a/src/definitions.h
+++ b/src/definitions.h
@@ -1,324 +1,324 @@
/***************************************************************************
* Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***************************************************************************/
#ifndef DEFINITIONS_H
#define DEFINITIONS_H
#include "gentime.h"
#include "kdenlive_debug.h"
#include
#include
#include
#include
#include
#include
const int MAXCLIPDURATION = 15000;
namespace Kdenlive {
enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 };
const int DefaultThumbHeight = 100;
} // namespace Kdenlive
enum class GroupType {
Normal,
Selection, // in that case, the group is used to emulate a selection
AVSplit, // in that case, the group links the audio and video of the same clip
Leaf // This is a leaf (clip or composition)
};
const QString groupTypeToStr(GroupType t);
GroupType groupTypeFromStr(const QString &s);
-enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, NoItem };
+enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, Master, NoItem };
using ObjectId = std::pair;
enum OperationType {
None = 0,
WaitingForConfirm,
MoveOperation,
ResizeStart,
ResizeEnd,
RollingStart,
RollingEnd,
RippleStart,
RippleEnd,
FadeIn,
FadeOut,
TransitionStart,
TransitionEnd,
MoveGuide,
KeyFrame,
Seek,
Spacer,
RubberSelection,
ScrollTimeline,
ZoomTimeline
};
namespace PlaylistState {
Q_NAMESPACE
enum ClipState { VideoOnly = 1, AudioOnly = 2, Disabled = 3 };
Q_ENUM_NS(ClipState)
} // namespace PlaylistState
// returns a pair corresponding to (video, audio)
std::pair stateToBool(PlaylistState::ClipState state);
PlaylistState::ClipState stateFromBool(std::pair av);
namespace TimelineMode {
enum EditMode { NormalEdit = 0, OverwriteEdit = 1, InsertEdit = 2 };
}
namespace ClipType {
Q_NAMESPACE
enum ProducerType {
Unknown = 0,
Audio = 1,
Video = 2,
AV = 3,
Color = 4,
Image = 5,
Text = 6,
SlideShow = 7,
Virtual = 8,
Playlist = 9,
WebVfx = 10,
TextTemplate = 11,
QText = 12,
Composition = 13,
Track = 14
};
Q_ENUM_NS(ProducerType)
} // namespace ClipType
enum ProjectItemType { ProjectClipType = 0, ProjectFolderType, ProjectSubclipType };
enum GraphicsRectItem { AVWidget = 70000, LabelWidget, TransitionWidget, GroupWidget };
enum ProjectTool { SelectTool = 0, RazorTool = 1, SpacerTool = 2 };
enum MonitorSceneType {
MonitorSceneNone = 0,
MonitorSceneDefault,
MonitorSceneGeometry,
MonitorSceneCorners,
MonitorSceneRoto,
MonitorSceneSplit,
MonitorSceneRipple
};
enum MessageType { DefaultMessage, ProcessingJobMessage, OperationCompletedMessage, InformationMessage, ErrorMessage, MltError };
enum TrackType { AudioTrack = 0, VideoTrack = 1, AnyTrack = 2 };
enum CacheType { SystemCacheRoot = -1, CacheRoot = 0, CacheBase = 1, CachePreview = 2, CacheProxy = 3, CacheAudio = 4, CacheThumbs = 5 };
enum TrimMode { NormalTrim, RippleTrim, RollingTrim, SlipTrim, SlideTrim };
class TrackInfo
{
public:
TrackType type;
QString trackName;
bool isMute;
bool isBlind;
bool isLocked;
int duration;
/*EffectsList effectsList;
TrackInfo()
: type(VideoTrack)
, isMute(0)
, isBlind(0)
, isLocked(0)
, duration(0)
, effectsList(true)
{
}*/
};
struct requestClipInfo
{
QDomElement xml;
QString clipId;
int imageHeight;
bool replaceProducer;
bool operator==(const requestClipInfo &a) const { return clipId == a.clipId; }
};
typedef QMap stringMap;
typedef QMap> audioByteArray;
using audioShortVector = QVector;
class ItemInfo
{
public:
/** startPos is the position where the clip starts on the track */
GenTime startPos;
/** endPos is the duration where the clip ends on the track */
GenTime endPos;
/** cropStart is the position where the sub-clip starts, relative to the clip's 0 position */
GenTime cropStart;
/** cropDuration is the duration of the clip */
GenTime cropDuration;
/** Track number */
int track{0};
ItemInfo() = default;
bool isValid() const { return startPos != endPos; }
bool contains(GenTime pos) const
{
if (startPos == endPos) {
return true;
}
return (pos <= endPos && pos >= startPos);
}
bool operator==(const ItemInfo &a) const { return startPos == a.startPos && endPos == a.endPos && track == a.track && cropStart == a.cropStart; }
};
class TransitionInfo
{
public:
/** startPos is the position where the clip starts on the track */
GenTime startPos;
/** endPos is the duration where the clip ends on the track */
GenTime endPos;
/** the track on which the transition is (b_track)*/
int b_track{0};
/** the track on which the transition is applied (a_track)*/
int a_track{0};
/** Does the user request for a special a_track */
bool forceTrack{0};
TransitionInfo() = default;
};
class CommentedTime
{
public:
CommentedTime();
CommentedTime(const GenTime &time, QString comment, int markerType = 0);
CommentedTime(const QString &hash, const GenTime &time);
QString comment() const;
GenTime time() const;
/** @brief Returns a string containing infos needed to store marker info. string equals marker type + QLatin1Char(':') + marker comment */
QString hash() const;
void setComment(const QString &comm);
void setMarkerType(int t);
int markerType() const;
static QColor markerColor(int type);
/* Implementation of > operator; Works identically as with basic types. */
bool operator>(const CommentedTime &op) const;
/* Implementation of < operator; Works identically as with basic types. */
bool operator<(const CommentedTime &op) const;
/* Implementation of >= operator; Works identically as with basic types. */
bool operator>=(const CommentedTime &op) const;
/* Implementation of <= operator; Works identically as with basic types. */
bool operator<=(const CommentedTime &op) const;
/* Implementation of == operator; Works identically as with basic types. */
bool operator==(const CommentedTime &op) const;
/* Implementation of != operator; Works identically as with basic types. */
bool operator!=(const CommentedTime &op) const;
private:
GenTime m_time;
QString m_comment;
int m_type{0};
};
QDebug operator<<(QDebug qd, const ItemInfo &info);
// we provide hash function for qstring and QPersistentModelIndex
namespace std {
template <> struct hash
{
std::size_t operator()(const QString &k) const { return qHash(k); }
};
template <> struct hash
{
std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); }
};
} // namespace std
// The following is a hack that allows to use shared_from_this in the case of a multiple inheritance.
// Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance
template struct enable_shared_from_this_virtual;
class enable_shared_from_this_virtual_base : public std::enable_shared_from_this
{
using base_type = std::enable_shared_from_this;
template friend struct enable_shared_from_this_virtual;
std::shared_ptr shared_from_this() { return base_type::shared_from_this(); }
std::shared_ptr shared_from_this() const { return base_type::shared_from_this(); }
};
template struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base
{
using base_type = enable_shared_from_this_virtual_base;
public:
std::shared_ptr shared_from_this()
{
std::shared_ptr result(base_type::shared_from_this(), static_cast(this));
return result;
}
std::shared_ptr shared_from_this() const
{
std::shared_ptr result(base_type::shared_from_this(), static_cast(this));
return result;
}
};
// This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance
// Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object
template class QAbstractItemModel_shared_from_this : public QAbstractItemModel
{
protected:
QAbstractItemModel_shared_from_this()
: QAbstractItemModel()
{
}
public:
std::shared_ptr shared_from_this()
{
std::shared_ptr p(weak_this_);
assert(p.get() == this);
return p;
}
std::shared_ptr shared_from_this() const
{
std::shared_ptr p(weak_this_);
assert(p.get() == this);
return p;
}
public: // actually private, but avoids compiler template friendship issues
mutable std::weak_ptr weak_this_;
};
#endif
diff --git a/src/timeline2/model/builders/meltBuilder.cpp b/src/timeline2/model/builders/meltBuilder.cpp
index 588107b62..d7add6284 100644
--- a/src/timeline2/model/builders/meltBuilder.cpp
+++ b/src/timeline2/model/builders/meltBuilder.cpp
@@ -1,363 +1,359 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "meltBuilder.hpp"
#include "../clipmodel.hpp"
#include "../timelineitemmodel.hpp"
#include "../timelinemodel.hpp"
#include "../trackmodel.hpp"
#include "../undohelper.hpp"
#include "bin/bin.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "kdenlivesettings.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static QStringList m_errorMessage;
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog = nullptr);
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog = nullptr);
bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor, QProgressDialog *progressDialog)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
// First, we destruct the previous tracks
timeline->requestReset(undo, redo);
m_errorMessage.clear();
std::unordered_map binIdCorresp;
pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, progressDialog);
QSet reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"),
QLatin1String("black_track")};
bool ok = true;
qDebug() << "//////////////////////\nTrying to construct" << tractor.count() << "tracks.\n////////////////////////////////";
// Import master track effects
- for (int i = 0; i < tractor.filter_count(); i++) {
- std::unique_ptr filter(tractor.filter(i));
- if (filter->get_int("internal_added") > 0) {
- timeline->tractor()->attach(*filter.get());
- }
- }
+ std::shared_ptr serv = std::make_shared(tractor.get_service());
+ timeline->importMasterEffects(serv);
QList videoTracksIndexes;
QList lockedTracksIndexes;
// Black track index
videoTracksIndexes << 0;
for (int i = 0; i < tractor.count() && ok; i++) {
std::unique_ptr track(tractor.track(i));
QString playlist_name = track->get("id");
if (reserved_names.contains(playlist_name)) {
continue;
}
switch (track->type()) {
case producer_type:
// TODO check that it is the black track, and otherwise log an error
qDebug() << "SUSPICIOUS: we weren't expecting a producer when parsing the timeline";
break;
case tractor_type: {
// that is a double track
int tid;
bool audioTrack = track->get_int("kdenlive:audio_track") == 1;
if (!audioTrack) {
videoTracksIndexes << i;
}
ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false);
if (track->get_int("kdenlive:locked_track") > 0) {
lockedTracksIndexes << tid;
}
Mlt::Tractor local_tractor(*track);
ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo, audioTrack, progressDialog);
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
break;
}
case playlist_type: {
// that is a single track
qDebug() << "Adding track: " << track->get("id");
int tid;
Mlt::Playlist local_playlist(*track);
const QString trackName = local_playlist.get("kdenlive:track_name");
bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1;
if (!audioTrack) {
videoTracksIndexes << i;
}
ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false);
int muteState = track->get_int("hide");
if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
}
ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo, audioTrack, progressDialog);
if (local_playlist.get_int("kdenlive:locked_track") > 0) {
lockedTracksIndexes << tid;
}
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
break;
}
default:
qDebug() << "ERROR: Unexpected item in the timeline";
}
}
timeline->_resetView();
// Loading compositions
QScopedPointer service(tractor.producer());
QList compositions;
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString id(t.get("kdenlive_id"));
QString internal(t.get("internal_added"));
if (internal.isEmpty()) {
compositions << new Mlt::Transition(t);
if (id.isEmpty()) {
qDebug() << "// Warning, this should not happen, transition without id: " << t.get("id") << " = " << t.get("mlt_service");
t.set("kdenlive_id", t.get("mlt_service"));
}
}
}
service.reset(service->producer());
}
// Sort compositions and insert
bool compositionOk = true;
while (!compositions.isEmpty()) {
QScopedPointer t(compositions.takeFirst());
QString id(t->get("kdenlive_id"));
int compoId;
int aTrack = t->get_a_track();
if (aTrack > tractor.count()) {
m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(),
t->get_in(), t->get_a_track());
continue;
}
if (t->get_int("force_track") == 0) {
// This is an automatic composition, check that we composite with lower track or warn
int pos = videoTracksIndexes.indexOf(t->get_b_track());
if (pos > 0 && videoTracksIndexes.at(pos - 1) != aTrack) {
t->set("force_track", 1);
m_errorMessage << i18n("Incorrect composition %1 found on track %2 at %3, compositing with track %4 was set to forced track.", t->get("id"), t->get_b_track(),
t->get_in(), t->get_a_track());
}
}
auto transProps = std::make_unique(t->get_properties());
compositionOk = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), t->get_length(), std::move(transProps), compoId, undo, redo);
if (!compositionOk) {
qDebug() << "ERROR : failed to insert composition in track " << t->get_b_track() << ", position" << t->get_in() << ", ID: " << id
<< ", MLT ID: " << t->get("id");
// timeline->requestItemDeletion(compoId, false);
m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3.", t->get("id"), t->get_b_track(), t->get_in());
continue;
}
qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in() << "/" << t->get_out();
}
// build internal track compositing
timeline->buildTrackCompositing();
// load locked state as last step
for (int tid : lockedTracksIndexes) {
timeline->setTrackLockedState(tid, true);
}
if (!ok) {
// TODO log error
// Don't abort loading because of failed composition
undo();
return false;
}
if (!m_errorMessage.isEmpty()) {
KMessageBox::sorry(qApp->activeWindow(), m_errorMessage.join("\n"), i18n("Problems found in your project file"));
}
return true;
}
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog)
{
if (track.count() != 2) {
// we expect a tractor with two tracks (a "fake" track)
qDebug() << "ERROR : wrong number of subtracks";
return false;
}
for (int i = 0; i < track.count(); i++) {
std::unique_ptr sub_track(track.track(i));
if (sub_track->type() != playlist_type) {
qDebug() << "ERROR : SubTracks must be MLT::Playlist";
return false;
}
Mlt::Playlist playlist(*sub_track);
constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo, audioTrack, progressDialog);
if (i == 0) {
// Pass track properties
int height = track.get_int("kdenlive:trackheight");
timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height));
timeline->setTrackProperty(tid, "kdenlive:collapsed", QString::number(track.get_int("kdenlive:collapsed")));
QString trackName = track.get("kdenlive:track_name");
if (!trackName.isEmpty()) {
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData());
}
if (audioTrack) {
// This is an audio track
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1"));
timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("1"));
} else {
// video track, hide audio
timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("2"));
}
int muteState = playlist.get_int("hide");
if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
}
}
}
std::shared_ptr serv = std::make_shared(track.get_service());
timeline->importTrackEffects(tid, serv);
return true;
}
namespace {
// This function tries to recover the state of the producer (audio or video or both)
PlaylistState::ClipState inferState(const std::shared_ptr &prod, bool audioTrack)
{
auto getProperty = [prod](const QString &name) {
if (prod->parent().is_valid()) {
return QString::fromUtf8(prod->parent().get(name.toUtf8().constData()));
}
return QString::fromUtf8(prod->get(name.toUtf8().constData()));
};
auto getIntProperty = [prod](const QString &name) {
if (prod->parent().is_valid()) {
return prod->parent().get_int(name.toUtf8().constData());
}
return prod->get_int(name.toUtf8().constData());
};
QString service = getProperty("mlt_service");
std::pair VidAud{true, true};
VidAud.first = getIntProperty("set.test_image") == 0;
VidAud.second = getIntProperty("set.test_audio") == 0;
if (audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1))) {
VidAud.first = false;
}
if (!audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1))) {
VidAud.second = false;
}
return stateFromBool(VidAud);
}
} // namespace
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog)
{
for (int i = 0; i < track.count(); i++) {
if (track.is_blank(i)) {
continue;
}
if (progressDialog) {
progressDialog->setValue(progressDialog->value() + 1);
}
std::shared_ptr clip(track.get_clip(i));
int position = track.clip_start(i);
switch (clip->type()) {
case unknown_type:
case producer_type: {
qDebug() << "Looking for clip clip " << clip->parent().get("kdenlive:id") << " = " << clip->parent().get("kdenlive:clipname");
QString binId;
if (clip->parent().get_int("_kdenlive_processed") == 1) {
// This is a bin clip, already processed no need to change id
binId = QString(clip->parent().get("kdenlive:id"));
} else {
QString clipId = clip->parent().get("kdenlive:id");
if (clipId.startsWith(QStringLiteral("slowmotion"))) {
clipId = clipId.section(QLatin1Char(':'), 1, 1);
}
if (clipId.isEmpty()) {
clipId = clip->get("kdenlive:id");
}
if (binIdCorresp.count(clipId) == 0) {
// Project was somehow corrupted
qDebug()<<"=== WARNING, CANNOT FIND CLIP WITH ID: "<projectItemModel()->getClipByUrl(QFileInfo(clip->parent().get("resource")));
if (!fixedId.isEmpty()) {
binId = fixedId.first();
m_errorMessage << i18n("Invalid clip %1 (%2) not found in project bin, recovered.", clip->parent().get("id"), clipId);
} else {
qWarning()<<"Warning, clip in timeline has no parent in bin: "<parent().get("id");
m_errorMessage << i18n("Project corrupted. Clip %1 (%2) not found in project bin.", clip->parent().get("id"), clipId);
}
} else {
binId = binIdCorresp.at(clipId);
}
Q_ASSERT(!clipId.isEmpty() && !binId.isEmpty());
clip->parent().set("kdenlive:id", binId.toUtf8().constData());
clip->parent().set("_kdenlive_processed", 1);
}
bool ok = false;
int cid = -1;
if (pCore->bin()->getBinClip(binId)) {
PlaylistState::ClipState st = inferState(clip, audioTrack);
cid = ClipModel::construct(timeline, binId, clip, st);
ok = timeline->requestClipMove(cid, tid, position, true, true, false, true, undo, redo);
} else {
qDebug() << "// Cannot find bin clip: " << binId << " - " << clip->get("id");
}
if (!ok && cid > -1) {
qDebug() << "ERROR : failed to insert clip in track" << tid << "position" << position;
timeline->requestItemDeletion(cid, false);
m_errorMessage << i18n("Invalid clip %1 found on track %2 at %3.", clip->parent().get("id"), track.get("id"), position);
break;
}
qDebug() << "Inserted clip in track" << tid << "at " << position;
break;
}
case tractor_type: {
// TODO This is a nested timeline
qDebug() << "NOT_IMPLEMENTED: code for parsing nested timeline is not there yet.";
break;
}
default:
qDebug() << "ERROR : unexpected object found on playlist";
return false;
break;
}
}
std::shared_ptr serv = std::make_shared(track.get_service());
timeline->importTrackEffects(tid, serv);
return true;
}
diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index 5b61f64be..cd447eb3e 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,3599 +1,3620 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "kdenlivesettings.h"
#include "logger.hpp"
#include "snapmodel.hpp"
#include "timelinefunctions.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "macros.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
using namespace rttr;
registration::class_("TimelineModel")
.method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
.method("requestClipMove", select_overload(&TimelineModel::requestClipMove))(
parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline"))
.method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))(
parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
.method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))(
parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
.method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
.method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))(
parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo"))
.method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
.method("requestItemResize", select_overload(&TimelineModel::requestItemResize))(
parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
.method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
parameter_names("itemIds", "logUndo", "type"))
.method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
.method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
.method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))(
parameter_names("pos", "id", "trackName", "audioTrack"))
.method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
.method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
.method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
.method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
.method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
.method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))(
parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
.method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))(
parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
.method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
.method("suggestCompositionMove",
&TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
// .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
// .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
// .method("requestCompositionInsertion", select_overload, int &, bool)>(
// &TimelineModel::requestCompositionInsertion))(
// parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
.method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","changeDuration"));
}
int TimelineModel::next_id = 0;
int TimelineModel::seekDuration = 30000;
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack)
: QAbstractItemModel_shared_from_this()
, m_blockRefresh(false)
, m_tractor(new Mlt::Tractor(*profile))
+ , m_masterStack(nullptr)
, m_snaps(new SnapModel())
, m_undoStack(std::move(undo_stack))
, m_profile(profile)
, m_blackClip(new Mlt::Producer(*profile, "color:black"))
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
, m_editMode(TimelineMode::NormalEdit)
, m_closing(false)
{
// Create black background track
m_blackClip->set("id", "black_track");
m_blackClip->set("mlt_type", "producer");
m_blackClip->set("aspect_ratio", 1);
m_blackClip->set("length", INT_MAX);
m_blackClip->set("set.test_audio", 0);
m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
m_tractor->insert_track(*m_blackClip, 0);
TRACE_CONSTR(this);
}
void TimelineModel::prepareClose()
{
requestClearSelection(true);
QWriteLocker locker(&m_lock);
// Unlock all tracks to allow delting clip from tracks
m_closing = true;
auto it = m_allTracks.begin();
while (it != m_allTracks.end()) {
(*it)->unlock();
++it;
}
}
TimelineModel::~TimelineModel()
{
std::vector all_ids;
for (auto tracks : m_iteratorTable) {
all_ids.push_back(tracks.first);
}
for (auto tracks : all_ids) {
deregisterTrack_lambda(tracks, false)();
}
for (const auto &clip : m_allClips) {
clip.second->deregisterClipToBin();
}
}
int TimelineModel::getTracksCount() const
{
READ_LOCK();
int count = m_tractor->count();
if (m_overlayTrackCount > -1) {
count -= m_overlayTrackCount;
}
Q_ASSERT(count >= 0);
// don't count the black background track
Q_ASSERT(count - 1 == static_cast(m_allTracks.size()));
return count - 1;
}
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
READ_LOCK();
auto it = m_allTracks.cbegin();
while (pos > 0) {
it++;
pos--;
}
return (*it)->getId();
}
int TimelineModel::getClipsCount() const
{
READ_LOCK();
int size = int(m_allClips.size());
return size;
}
int TimelineModel::getCompositionsCount() const
{
READ_LOCK();
int size = int(m_allCompositions.size());
return size;
}
int TimelineModel::getClipTrackId(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
return clip->getCurrentTrackId();
}
int TimelineModel::getCompositionTrackId(int compoId) const
{
Q_ASSERT(m_allCompositions.count(compoId) > 0);
const auto trans = m_allCompositions.at(compoId);
return trans->getCurrentTrackId();
}
int TimelineModel::getItemTrackId(int itemId) const
{
READ_LOCK();
Q_ASSERT(isItem(itemId));
if (isComposition(itemId)) {
return getCompositionTrackId(itemId);
}
return getClipTrackId(itemId);
}
int TimelineModel::getClipPosition(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
int pos = clip->getPosition();
return pos;
}
double TimelineModel::getClipSpeed(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId)->getSpeed();
}
int TimelineModel::getClipSplitPartner(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_groups->getSplitPartner(clipId);
}
int TimelineModel::getClipIn(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
return clip->getIn();
}
PlaylistState::ClipState TimelineModel::getClipState(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
return clip->clipState();
}
const QString TimelineModel::getClipBinId(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
QString id = clip->binId();
return id;
}
int TimelineModel::getClipPlaytime(int clipId) const
{
READ_LOCK();
Q_ASSERT(isClip(clipId));
const auto clip = m_allClips.at(clipId);
int playtime = clip->getPlaytime();
return playtime;
}
QSize TimelineModel::getClipFrameSize(int clipId) const
{
READ_LOCK();
Q_ASSERT(isClip(clipId));
const auto clip = m_allClips.at(clipId);
return clip->getFrameSize();
}
int TimelineModel::getTrackClipsCount(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
int count = getTrackById_const(trackId)->getClipsCount();
return count;
}
int TimelineModel::getClipByPosition(int trackId, int position) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getClipByPosition(position);
}
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getCompositionByPosition(position);
}
int TimelineModel::getTrackPosition(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_allTracks.cbegin();
int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
return pos;
}
int TimelineModel::getTrackMltIndex(int trackId) const
{
READ_LOCK();
// Because of the black track that we insert in first position, the mlt index is the position + 1
return getTrackPosition(trackId) + 1;
}
int TimelineModel::getTrackSortValue(int trackId, int separated) const
{
if (separated == 1) {
// This will be A2, A1, V1, V2
return getTrackPosition(trackId) + 1;
}
if (separated == 2) {
// This will be A1, A2, V1, V2
// Count audio/video tracks
auto it = m_allTracks.cbegin();
int aCount = 0;
int vCount = 0;
int refPos = 0;
bool isVideo = true;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
if ((*it)->getId() == trackId) {
refPos = aCount;
isVideo = false;
}
aCount++;
} else {
// video track
if ((*it)->getId() == trackId) {
refPos = vCount;
}
vCount++;
}
++it;
}
return isVideo ? aCount + refPos + 1 : aCount - refPos;
}
// This will be A1, V1, A2, V2
auto it = m_allTracks.cend();
int aCount = 0;
int vCount = 0;
bool isAudio = false;
int trackPos = 0;
while (it != m_allTracks.begin()) {
--it;
bool audioTrack = (*it)->isAudioTrack();
if (audioTrack) {
aCount++;
} else {
vCount++;
}
if (trackId == (*it)->getId()) {
isAudio = audioTrack;
trackPos = audioTrack ? aCount : vCount;
}
}
int trackDiff = qMax(0, aCount - vCount);
if (trackDiff > 0) {
// more audio tracks, keep them below
if (isAudio && trackPos > vCount) {
return -trackPos;
}
}
return isAudio ? 2 * trackPos : 2 * (vCount + 1 - trackPos) + 1;
}
QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
QList results;
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (type == TrackType::AnyTrack) {
results << (*it)->getId();
continue;
}
bool audioTrack = (*it)->isAudioTrack();
if (type == TrackType::AudioTrack && audioTrack) {
results << (*it)->getId();
} else if (type == TrackType::VideoTrack && !audioTrack) {
results << (*it)->getId();
}
}
return results;
}
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return (*it)->getId();
}
}
return 0;
}
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return getTrackMltIndex((*it)->getId());
}
}
return 0;
}
int TimelineModel::getMirrorVideoTrackId(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
if (!(*it)->isAudioTrack()) {
// we expected an audio track...
return -1;
}
int count = 0;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
++it;
}
return -1;
}
int TimelineModel::getMirrorTrackId(int trackId) const
{
if (isAudioTrack(trackId)) {
return getMirrorVideoTrackId(trackId);
}
return getMirrorAudioTrackId(trackId);
}
int TimelineModel::getMirrorAudioTrackId(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
if ((*it)->isAudioTrack()) {
// we expected a video track...
qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO";
return -1;
}
int count = 0;
while (it != m_allTracks.cbegin()) {
if (!(*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
--it;
}
if ((*it)->isAudioTrack() && count == 1) {
return (*it)->getId();
}
return -1;
}
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
m_editMode = mode;
}
bool TimelineModel::normalEdit() const
{
return m_editMode == TimelineMode::NormalEdit;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
{
Q_UNUSED(updateView);
Q_UNUSED(invalidateTimeline);
Q_UNUSED(undo);
Q_UNUSED(redo);
Q_ASSERT(isClip(clipId));
m_allClips[clipId]->setFakePosition(position);
bool trackChanged = false;
if (trackId > -1) {
if (trackId != m_allClips[clipId]->getFakeTrackId()) {
if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
m_allClips[clipId]->setFakeTrackId(trackId);
trackChanged = true;
}
}
}
QModelIndex modelIndex = makeClipIndexFromID(clipId);
if (modelIndex.isValid()) {
QVector roles{FakePositionRole};
if (trackChanged) {
roles << FakeTrackIdRole;
}
notifyChange(modelIndex, modelIndex, roles);
return true;
}
return false;
}
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<clipState() == PlaylistState::Disabled) {
if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
return false;
}
if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
return false;
}
} else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
// Move not allowed (audio / video mismatch)
qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
return false;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
bool ok = true;
int old_trackId = getClipTrackId(clipId);
bool notifyViewOnly = false;
// qDebug()<<"MOVING CLIP FROM: "<isAudioTrack()) {
int in = getClipPosition(clipId);
emit invalidateZone(in, in + getClipPlaytime(clipId));
}
return true;
};
}
if (old_trackId != -1) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
if (!ok) {
qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && m_allClips[clipId]->getFakeTrackId() == trackId) {
TRACE_RES(true);
qDebug()<<"........\nABORTING MOVE; SAME POS/TRACK\n..........";
return true;
}
if (m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_groups->getRootId(clipId);
int current_trackId = getClipTrackId(clipId);
int track_pos1 = getTrackPosition(trackId);
int track_pos2 = getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_allClips[clipId]->getPosition();
bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
TRACE_RES(res);
return res;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
TRACE_RES(true);
return true;
}
if (m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_groups->getRootId(clipId);
int current_trackId = getClipTrackId(clipId);
int track_pos1 = getTrackPosition(trackId);
int track_pos2 = getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_allClips[clipId]->getPosition();
return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
return true;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = true;
if (m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_groups->getRootId(clipId);
int current_trackId = getClipTrackId(clipId);
int track_pos1 = getTrackPosition(trackId);
int track_pos2 = getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_allClips[clipId]->getPosition();
res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
} else {
res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
}
if (res) {
undo();
}
return res;
}
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
{
if (isClip(itemId)) {
return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
}
return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
}
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, cursorPosition, snapDistance);
Q_ASSERT(isClip(clipId));
Q_ASSERT(isTrack(trackId));
int currentPos = getClipPosition(clipId);
int sourceTrackId = getClipTrackId(clipId);
if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
// Trying move on incompatible track type, stay on same track
trackId = sourceTrackId;
}
if (currentPos == position && m_editMode == TimelineMode::NormalEdit && sourceTrackId == trackId) {
TRACE_RES(position);
return position;
}
bool after = position > currentPos;
if (snapDistance > 0) {
// For snapping, we must ignore all in/outs of the clips of the group being moved
std::vector ignored_pts;
std::unordered_set all_items = {clipId};
if (m_groups->isInGroup(clipId)) {
int groupId = m_groups->getRootId(clipId);
all_items = m_groups->getLeaves(groupId);
}
for (int current_clipId : all_items) {
if (getItemTrackId(current_clipId) != -1) {
int in = getItemPosition(current_clipId);
int out = in + getItemPlaytime(current_clipId);
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
}
int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(),
cursorPosition, snapDistance);
// qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
if (snapped >= 0) {
position = snapped;
}
}
// we check if move is possible
bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false)
: requestFakeClipMove(clipId, trackId, position, true, false, false);
/*} else {
possible = requestClipMoveAttempt(clipId, trackId, position);
}*/
if (possible) {
TRACE_RES(position);
return position;
}
if (sourceTrackId == -1) {
// not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away...
TRACE_RES(currentPos);
return currentPos;
}
// Find best possible move
if (!m_groups->isInGroup(clipId)) {
// Try same track move
if (trackId != sourceTrackId && sourceTrackId != -1) {
qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
trackId = sourceTrackId;
possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
if (!possible) {
qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
} else {
TRACE_RES(position);
return position;
}
}
int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
qDebug() << "Found blank" << blank_length;
if (blank_length < INT_MAX) {
if (after) {
position = currentPos + blank_length;
} else {
position = currentPos - blank_length;
}
} else {
TRACE_RES(currentPos);
return currentPos;
}
possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
TRACE_RES(possible ? position : currentPos);
return possible ? position : currentPos;
}
if (trackId != sourceTrackId) {
// Try same track move
possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false);
return possible ? position : currentPos;
}
// find best pos for groups
int groupId = m_groups->getRootId(clipId);
std::unordered_set all_items = m_groups->getLeaves(groupId);
QMap trackPosition;
// First pass, sort clips by track and keep only the first / last depending on move direction
for (int current_clipId : all_items) {
int clipTrack = getItemTrackId(current_clipId);
if (clipTrack == -1) {
continue;
}
int in = getItemPosition(current_clipId);
if (trackPosition.contains(clipTrack)) {
if (after) {
// keep only last clip position for track
int out = in + getItemPlaytime(current_clipId);
if (trackPosition.value(clipTrack) < out) {
trackPosition.insert(clipTrack, out);
}
} else {
// keep only first clip position for track
if (trackPosition.value(clipTrack) > in) {
trackPosition.insert(clipTrack, in);
}
}
} else {
trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
}
}
// Now check space on each track
QMapIterator i(trackPosition);
int blank_length = 0;
while (i.hasNext()) {
i.next();
int track_space;
if (!after) {
// Check space before the position
track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1);
if (blank_length == 0 || blank_length > track_space) {
blank_length = track_space;
}
} else {
// Check space after the position
track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
if (blank_length == 0 || blank_length > track_space) {
blank_length = track_space;
}
}
}
if (snapDistance > 0) {
if (blank_length > 10 * snapDistance) {
blank_length = 0;
}
} else if (blank_length / m_profile->fps() > 5) {
blank_length = 0;
}
if (blank_length != 0) {
int updatedPos = currentPos + (after ? blank_length : -blank_length);
possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false);
if (possible) {
TRACE_RES(updatedPos);
return updatedPos;
}
}
TRACE_RES(currentPos);
return currentPos;
}
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
{
QWriteLocker locker(&m_lock);
TRACE(compoId, trackId, position, cursorPosition, snapDistance);
Q_ASSERT(isComposition(compoId));
Q_ASSERT(isTrack(trackId));
int currentPos = getCompositionPosition(compoId);
int currentTrack = getCompositionTrackId(compoId);
if (getTrackById_const(trackId)->isAudioTrack()) {
// Trying move on incompatible track type, stay on same track
trackId = currentTrack;
}
if (currentPos == position && currentTrack == trackId) {
TRACE_RES(position);
return position;
}
if (snapDistance > 0) {
// For snapping, we must ignore all in/outs of the clips of the group being moved
std::vector ignored_pts;
if (m_groups->isInGroup(compoId)) {
int groupId = m_groups->getRootId(compoId);
auto all_items = m_groups->getLeaves(groupId);
for (int current_compoId : all_items) {
// TODO: fix for composition
int in = getItemPosition(current_compoId);
int out = in + getItemPlaytime(current_compoId);
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
} else {
int in = currentPos;
int out = in + getCompositionPlaytime(compoId);
qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out;
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance);
qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped;
if (snapped >= 0) {
position = snapped;
}
}
// we check if move is possible
bool possible = requestCompositionMove(compoId, trackId, position, true, false);
qDebug() << "Original move success" << possible;
if (possible) {
TRACE_RES(position);
return position;
}
/*bool after = position > currentPos;
int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after);
qDebug() << "Found blank" << blank_length;
if (blank_length < INT_MAX) {
if (after) {
return currentPos + blank_length;
}
return currentPos - blank_length;
}
return position;*/
TRACE_RES(currentPos);
return currentPos;
}
bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo)
{
qDebug() << "requestClipCreation " << binClipId;
QString bid = binClipId;
if (binClipId.contains(QLatin1Char('/'))) {
bid = binClipId.section(QLatin1Char('/'), 0, 0);
}
if (!pCore->projectItemModel()->hasClip(bid)) {
qDebug() << " / / / /MASTER CLIP NOT FOUND";
return false;
}
std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid);
if (!master->isReady() || !master->isCompatible(state)) {
qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state;
return false;
}
int clipId = TimelineModel::getNextId();
id = clipId;
Fun local_undo = deregisterClip_lambda(clipId);
ClipModel::construct(shared_from_this(), bid, clipId, state, speed);
auto clip = m_allClips[clipId];
Fun local_redo = [clip, this, state]() {
// We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
// sufficient to register it.
registerClip(clip, true);
clip->refreshProducerFromBin(state);
return true;
};
if (binClipId.contains(QLatin1Char('/'))) {
int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt();
int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt();
int initLength = m_allClips[clipId]->getPlaytime();
bool res = true;
if (in != 0) {
res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo);
}
res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo);
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets)
{
QWriteLocker locker(&m_lock);
TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo);
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Insert Clip"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
Fun &undo, Fun &redo, QVector allowedTracks)
{
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
qDebug() << "requestClipInsertion " << binClipId << " "
<< " " << trackId << " " << position;
bool res = false;
ClipType::ProducerType type = ClipType::Unknown;
QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
// dropType indicates if we want a normal drop (disabled), audio only or video only drop
PlaylistState::ClipState dropType = PlaylistState::Disabled;
if (bid.startsWith(QLatin1Char('A'))) {
dropType = PlaylistState::AudioOnly;
bid = bid.remove(0, 1);
} else if (bid.startsWith(QLatin1Char('V'))) {
dropType = PlaylistState::VideoOnly;
bid = bid.remove(0, 1);
}
if (!pCore->projectItemModel()->hasClip(bid)) {
return false;
}
std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid);
type = master->clipType();
if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) {
useTargets = false;
}
if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) {
if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) {
// If audio target is set but no video target, only insert audio
trackId = m_audioTarget;
if (trackId > -1 && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
trackId = -1;
}
} else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
// Video target set but locked
trackId = m_audioTarget;
if (trackId > -1 && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
trackId = -1;
}
}
if (trackId == -1) {
if (!allowedTracks.isEmpty()) {
// No active tracks, aborting
return true;
}
pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
return false;
}
bool audioDrop = getTrackById_const(trackId)->isAudioTrack();
res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo);
res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
int target_track;
if (audioDrop) {
target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget;
} else {
target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget;
}
if (useTargets && !allowedTracks.contains(target_track)) {
target_track = -1;
}
qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo();
int mirror = getMirrorTrackId(trackId);
if (mirror > -1 && getTrackById_const(mirror)->isLocked()) {
mirror = -1;
}
bool canMirrorDrop = !useTargets && mirror > -1;
if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) {
if (!useTargets) {
target_track = mirror;
}
// QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack);
QList possibleTracks;
qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets;
if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) {
possibleTracks << target_track;
}
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
res = false;
} else {
std::function audio_undo = []() { return true; };
std::function audio_redo = []() { return true; };
int newId;
res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo);
if (res) {
bool move = false;
while (!move && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
move = requestClipMove(newId, newTrack, position, true, true, true, true, audio_undo, audio_redo);
}
// use lazy evaluation to group only if move was successful
res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit);
if (!res || !move) {
pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
} else {
UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
}
} else {
pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
}
}
}
} else {
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid);
if (dropType == PlaylistState::Disabled) {
dropType = getTrackById_const(trackId)->trackType();
} else if (dropType != getTrackById_const(trackId)->trackType()) {
qDebug() << "// INCORRECT DRAG, ABORTING";
return false;
}
QString normalisedBinId = binClipId;
if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
normalisedBinId.remove(0, 1);
}
res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo);
res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
}
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
id = -1;
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (m_groups->isInGroup(itemId)) {
return requestGroupDeletion(itemId, undo, redo);
}
if (isClip(itemId)) {
return requestClipDeletion(itemId, undo, redo);
}
if (isComposition(itemId)) {
return requestCompositionDeletion(itemId, undo, redo);
}
Q_ASSERT(false);
return false;
}
bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, logUndo);
Q_ASSERT(isItem(itemId));
QString actionLabel;
if (m_groups->isInGroup(itemId)) {
actionLabel = i18n("Remove group");
} else {
if (isClip(itemId)) {
actionLabel = i18n("Delete Clip");
} else {
actionLabel = i18n("Delete Composition");
}
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = requestItemDeletion(itemId, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, actionLabel);
}
TRACE_RES(res);
requestClearSelection(true);
return res;
}
bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo)
{
int trackId = getClipTrackId(clipId);
if (trackId != -1) {
bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo, false, true);
if (!res) {
undo();
return false;
}
}
auto operation = deregisterClip_lambda(clipId);
auto clip = m_allClips[clipId];
Fun reverse = [this, clip]() {
// We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
// sufficient to register it.
registerClip(clip, true);
return true;
};
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
undo();
return false;
}
bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo)
{
int trackId = getCompositionTrackId(compositionId);
if (trackId != -1) {
bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true);
if (!res) {
undo();
return false;
} else {
Fun unplant_op = [this, compositionId]() {
unplantComposition(compositionId);
return true;
};
unplant_op();
PUSH_LAMBDA(unplant_op, redo);
}
}
Fun operation = deregisterComposition_lambda(compositionId);
auto composition = m_allCompositions[compositionId];
int new_in = composition->getPosition();
int new_out = new_in + composition->getPlaytime();
Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() {
// We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
// back it is sufficient to register it.
registerComposition(composition);
composition->setCurrentTrackId(trackId, true);
replantCompositions(compositionId, false);
checkRefresh(new_in, new_out);
return true;
};
if (operation()) {
Fun update_monitor = [this, new_in, new_out]() {
checkRefresh(new_in, new_out);
return true;
};
update_monitor();
PUSH_LAMBDA(update_monitor, operation);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
undo();
return false;
}
std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions)
{
Q_UNUSED(listCompositions)
std::unordered_set allClips;
if (trackId == -1) {
for (const auto &track : m_allTracks) {
if (track->isLocked()) {
continue;
}
std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions);
allClips.insert(clipTracks.begin(), clipTracks.end());
}
} else {
std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end);
allClips.insert(clipTracks.begin(), clipTracks.end());
if (listCompositions) {
std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end);
allClips.insert(compoTracks.begin(), compoTracks.end());
}
}
return allClips;
}
bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
{
TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move group"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
bool allowViewRefresh)
{
Q_UNUSED(updateView);
Q_UNUSED(finalMove);
Q_UNUSED(undo);
Q_UNUSED(redo);
Q_UNUSED(allowViewRefresh);
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allGroups.count(groupId) > 0);
bool ok = true;
auto all_items = m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
// Check if there is a track move
// First, remove clips
std::unordered_map old_track_ids, old_position, old_forced_track;
for (int item : all_items) {
int old_trackId = getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
if (isClip(item)) {
old_position[item] = m_allClips[item]->getPosition();
} else {
old_position[item] = m_allCompositions[item]->getPosition();
old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
}
}
}
// Second step, calculate delta
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
if (getTrackById(old_track_ids[clipId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
bool trackChanged = false;
// Reverse sort. We need to insert from left to right to avoid confusing the view
for (int item : all_items) {
int current_track_id = old_track_ids[item];
int current_track_position = getTrackPosition(current_track_id);
int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
if (target_track_position >= 0 && target_track_position < getTracksCount()) {
auto it = m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
int target_position = old_position[item] + delta_pos;
if (isClip(item)) {
qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position;
m_allClips[item]->setFakePosition(target_position);
if (m_allClips[item]->getFakeTrackId() != target_track) {
trackChanged = true;
}
m_allClips[item]->setFakeTrackId(target_track);
} else {
}
} else {
qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n..";
ok = false;
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
QModelIndex modelIndex;
QVector roles{FakePositionRole};
if (trackChanged) {
roles << FakeTrackIdRole;
}
for (int item : all_items) {
if (isClip(item)) {
modelIndex = makeClipIndexFromID(item);
} else {
modelIndex = makeCompositionIndexFromID(item);
}
notifyChange(modelIndex, modelIndex, roles);
}
return true;
}
bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, moveMirrorTracks);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move group"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool moveMirrorTracks,
bool allowViewRefresh, QVector allowedTracks)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allGroups.count(groupId) > 0);
Q_ASSERT(isItem(itemId));
if (getGroupElements(groupId).count(itemId) == 0) {
// this group doesn't contain the clip, abort
return false;
}
bool ok = true;
auto all_items = m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
std::unordered_set all_clips;
std::unordered_set all_compositions;
// Separate clips from compositions to sort
for (int affectedItemId : all_items) {
if (isClip(affectedItemId)) {
all_clips.insert(affectedItemId);
} else {
all_compositions.insert(affectedItemId);
}
}
// Sort clips first
std::vector sorted_clips(all_clips.begin(), all_clips.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) {
int p1 = m_allClips[clipId1]->getPosition();
int p2 = m_allClips[clipId2]->getPosition();
return delta_pos > 0 ? p2 <= p1 : p1 <= p2;
});
// Sort compositions. We need to delete in the move direction from top to bottom
std::vector sorted_compositions(all_compositions.begin(), all_compositions.end());
std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) {
int p1 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition();
int p2 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition();
return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2;
});
sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end());
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
Fun update_model = [this, finalMove]() {
if (finalMove) {
updateDuration();
}
return true;
};
// Check if there is a track move
bool updatePositionOnly = false;
// Second step, reinsert clips at correct positions
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
if (delta_track == 0 && updateView) {
updateView = false;
allowViewRefresh = false;
updatePositionOnly = true;
update_model = [sorted_clips, finalMove, this]() {
QModelIndex modelIndex;
QVector roles{StartRole};
for (int item : sorted_clips) {
if (isClip(item)) {
modelIndex = makeClipIndexFromID(item);
} else {
modelIndex = makeCompositionIndexFromID(item);
}
notifyChange(modelIndex, modelIndex, roles);
}
if (finalMove) {
updateDuration();
}
return true;
};
}
std::unordered_map old_track_ids, old_position, old_forced_track;
// First, remove clips
if (delta_track != 0) {
// We delete our clips only if changing track
for (int item : sorted_clips) {
int old_trackId = getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = allowViewRefresh;
if (isClip(item)) {
ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, true, false);
old_position[item] = m_allClips[item]->getPosition();
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo);
old_position[item] = m_allCompositions[item]->getPosition();
old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
}
if (!moveMirrorTracks) {
if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = 0;
} else {
audio_delta = 0;
}
} else {
if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
}
}
// We need to insert depending on the move direction to avoid confusing the view
// std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
bool updateThisView = allowViewRefresh;
if (delta_track == 0) {
// Special case, we are moving on same track, avoid too many calculations
for (int item : sorted_clips) {
int current_track_id = getItemTrackId(item);
if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
continue;
}
int target_position = getItemPosition(item) + delta_pos;
if (isClip(item)) {
ok = ok && requestClipMove(item, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} else {
ok = ok &&
requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo);
}
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
} else {
// Track changed
for (int item : sorted_clips) {
int current_track_id = old_track_ids[item];
int current_track_position = getTrackPosition(current_track_id);
int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
if (target_track_position >= 0 && target_track_position < getTracksCount()) {
auto it = m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
int target_position = old_position[item] + delta_pos;
if (isClip(item)) {
ok = ok && requestClipMove(item, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} else {
ok = ok &&
requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo);
}
} else {
qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n..";
ok = false;
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
}
if (updatePositionOnly) {
update_model();
PUSH_LAMBDA(update_model, local_redo);
PUSH_LAMBDA(update_model, local_undo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, logUndo);
if (!m_groups->isInGroup(clipId)) {
TRACE_RES(false);
return false;
}
bool res = requestItemDeletion(clipId, logUndo);
TRACE_RES(res);
return res;
}
bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo)
{
// we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves.
std::queue group_queue;
group_queue.push(m_groups->getRootId(clipId));
std::unordered_set all_items;
std::unordered_set all_compositions;
while (!group_queue.empty()) {
int current_group = group_queue.front();
bool isSelection = m_currentSelection == current_group;
if (isSelection) {
m_currentSelection = -1;
}
group_queue.pop();
Q_ASSERT(isGroup(current_group));
auto children = m_groups->getDirectChildren(current_group);
int one_child = -1; // we need the id on any of the indices of the elements of the group
for (int c : children) {
if (isClip(c)) {
all_items.insert(c);
one_child = c;
} else if (isComposition(c)) {
all_compositions.insert(c);
one_child = c;
} else {
Q_ASSERT(isGroup(c));
one_child = c;
group_queue.push(c);
}
}
if (one_child != -1) {
if (m_groups->getType(current_group) == GroupType::Selection) {
Q_ASSERT(isSelection);
// in the case of a selection group, we delete the group but don't log it in the undo object
Fun tmp_undo = []() { return true; };
Fun tmp_redo = []() { return true; };
m_groups->ungroupItem(one_child, tmp_undo, tmp_redo);
} else {
bool res = m_groups->ungroupItem(one_child, undo, redo);
if (!res) {
undo();
return false;
}
}
}
}
for (int clip : all_items) {
bool res = requestClipDeletion(clip, undo, redo);
if (!res) {
// Undo is processed in requestClipDeletion
return false;
}
}
for (int compo : all_compositions) {
bool res = requestCompositionDeletion(compo, undo, redo);
if (!res) {
undo();
return false;
}
}
return true;
}
const QVariantList TimelineModel::getGroupData(int itemId)
{
QWriteLocker locker(&m_lock);
if (!m_groups->isInGroup(itemId)) {
return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)};
}
int groupId = m_groups->getRootId(itemId);
QVariantList result;
std::unordered_set items = m_groups->getLeaves(groupId);
for (int id : items) {
result << id << getItemPosition(id) << getItemPlaytime(id);
}
return result;
}
void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right)
{
Q_ASSERT(startPos.size() == endPos.size());
QMap> startData;
QMap> endData;
while (!startPos.isEmpty()) {
int id = startPos.takeFirst().toInt();
int in = startPos.takeFirst().toInt();
int duration = startPos.takeFirst().toInt();
startData.insert(id, {in, duration});
id = endPos.takeFirst().toInt();
in = endPos.takeFirst().toInt();
duration = endPos.takeFirst().toInt();
endData.insert(id, {in, duration});
}
QMapIterator> i(startData);
QList changedItems;
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
while (i.hasNext()) {
i.next();
QPair startItemPos = i.value();
QPair endItemPos = endData.value(i.key());
if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) {
// Revert individual items to original position
requestItemResize(i.key(), startItemPos.second, right, false, 0, true);
changedItems << i.key();
}
}
for (int id : changedItems) {
QPair endItemPos = endData.value(id);
result = result & requestItemResize(id, endItemPos.second, right, true, undo, redo, false);
if (!result) {
break;
}
}
if (result) {
PUSH_UNDO(undo, redo, i18n("Resize group"));
} else {
undo();
}
}
const std::vector TimelineModel::getBoundaries(int itemId)
{
std::vector boundaries;
std::unordered_set