diff --git a/data/effects/automask.xml b/data/effects/automask.xml
index d7d6a953a..94a32d29c 100644
--- a/data/effects/automask.xml
+++ b/data/effects/automask.xml
@@ -1,38 +1,42 @@
Auto Mask
Hide a selected zone and follow its movements
Zachary Drew
Geometry
Macroblock width
Macroblock height
Maximum x distance
Maximum y distance
Denoise
Debug
Obscure
+
+ Tracking data
+ Click to copy to clipboard
+
Analyse
motion_vector_list
autotrack_rectangle
Motion vectors
diff --git a/src/assets/view/widgets/clickablelabelwidget.cpp b/src/assets/view/widgets/clickablelabelwidget.cpp
index 0e4c5f5fb..07a5e3ed5 100644
--- a/src/assets/view/widgets/clickablelabelwidget.cpp
+++ b/src/assets/view/widgets/clickablelabelwidget.cpp
@@ -1,72 +1,83 @@
/***************************************************************************
* Copyright (C) 2019 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "clickablelabelwidget.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "jobs/filterclipjob.h"
#include "jobs/jobmanager.h"
#include "core.h"
#include
#include
#include
+#include
+#include
#include
#include
ClickableLabelParamWidget::ClickableLabelParamWidget(std::shared_ptr model, QModelIndex index, QWidget *parent)
: AbstractParamWidget(std::move(model), index, parent)
{
// setup the comment
m_displayName = m_model->data(m_index, Qt::DisplayRole).toString();
QString name = m_model->data(m_index, AssetParameterModel::NameRole).toString();
QString comment = m_model->data(m_index, AssetParameterModel::CommentRole).toString();
setToolTip(comment);
- auto *layout = new QVBoxLayout(this);
+ auto *layout = new QHBoxLayout(this);
+ QToolButton *tb = new QToolButton(this);
+ tb->setAutoRaise(true);
+ tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
m_label = new QLabel(this);
m_label->setWordWrap(true);
+ layout->addWidget(tb);
layout->addWidget(m_label);
- setMinimumHeight(m_label->sizeHint().height());
+ setMinimumHeight(tb->sizeHint().height());
+ connect(tb, &QToolButton::clicked, [&]() {
+ QClipboard *clipboard = QApplication::clipboard();
+ QString value = m_model->data(m_index, AssetParameterModel::ValueRole).toString();
+ clipboard->setText(value);
+ });
connect(m_label, &QLabel::linkActivated, [&](const QString &result) {
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(result);
});
slotRefresh();
}
void ClickableLabelParamWidget::slotShowComment(bool show)
{
Q_UNUSED(show);
/*if (!m_labelComment->text().isEmpty()) {
m_widgetComment->setVisible(show);
}*/
}
void ClickableLabelParamWidget::slotRefresh()
{
QString value = m_model->data(m_index, AssetParameterModel::ValueRole).toString();
m_label->setText(QStringLiteral("").arg(value) + m_displayName + QStringLiteral(""));
- m_label->setVisible(!value.isEmpty());
+ setVisible(!value.isEmpty());
}
bool ClickableLabelParamWidget::getValue()
{
return true;
}
diff --git a/src/bin/projectsubclip.cpp b/src/bin/projectsubclip.cpp
index 35991cd2e..36e5cd739 100644
--- a/src/bin/projectsubclip.cpp
+++ b/src/bin/projectsubclip.cpp
@@ -1,206 +1,205 @@
/*
Copyright (C) 2015 Jean-Baptiste Mardelle
This file is part of Kdenlive. See www.kdenlive.org.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License or (at your option) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "projectsubclip.h"
#include "projectclip.h"
#include "projectitemmodel.h"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "doc/docundostack.hpp"
#include "bincommands.h"
#include "jobs/jobmanager.h"
#include "jobs/cachejob.hpp"
#include "utils/thumbnailcache.hpp"
#include
#include
#include
#include
class ClipController;
ProjectSubClip::ProjectSubClip(const QString &id, const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out,
const QString &timecode, const QString &name)
: AbstractProjectItem(AbstractProjectItem::SubClipItem, id, model)
, m_masterClip(parent)
{
m_inPoint = in;
m_outPoint = out;
m_duration = timecode;
m_parentDuration = m_masterClip->frameDuration();
m_parentClipId = m_masterClip->clipId();
QPixmap pix(64, 36);
pix.fill(Qt::lightGray);
m_thumbnail = QIcon(pix);
if (name.isEmpty()) {
m_name = i18n("Zone %1", parent->childCount() + 1);
} else {
m_name = name;
}
m_clipStatus = StatusReady;
// Save subclip in MLT
connect(parent.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb);
}
std::shared_ptr ProjectSubClip::construct(const QString &id, const std::shared_ptr &parent,
const std::shared_ptr &model, int in, int out, const QString &timecode,
const QString &name)
{
std::shared_ptr self(new ProjectSubClip(id, parent, model, in, out, timecode, name));
baseFinishConstruct(self);
return self;
}
ProjectSubClip::~ProjectSubClip()
{
// controller is deleted in bincontroller
}
void ProjectSubClip::gotThumb(int pos, const QImage &img)
{
if (pos == m_inPoint) {
setThumbnail(img);
disconnect(m_masterClip.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb);
}
}
QString ProjectSubClip::getToolTip() const
{
return QString("%1-%2").arg(m_inPoint).arg(m_outPoint);
}
std::shared_ptr ProjectSubClip::clip(const QString &id)
{
Q_UNUSED(id);
return std::shared_ptr();
}
std::shared_ptr ProjectSubClip::folder(const QString &id)
{
Q_UNUSED(id);
return std::shared_ptr();
}
void ProjectSubClip::setBinEffectsEnabled(bool) {}
GenTime ProjectSubClip::duration() const
{
// TODO
return {};
}
QPoint ProjectSubClip::zone() const
{
return {m_inPoint, m_outPoint};
}
std::shared_ptr ProjectSubClip::clipAt(int ix)
{
Q_UNUSED(ix);
return std::shared_ptr();
}
QDomElement ProjectSubClip::toXml(QDomDocument &document, bool, bool)
{
QDomElement sub = document.createElement(QStringLiteral("subclip"));
sub.setAttribute(QStringLiteral("id"), m_masterClip->AbstractProjectItem::clipId());
sub.setAttribute(QStringLiteral("in"), m_inPoint);
sub.setAttribute(QStringLiteral("out"), m_outPoint);
return sub;
}
std::shared_ptr ProjectSubClip::subClip(int in, int out)
{
if (m_inPoint == in && m_outPoint == out) {
return std::static_pointer_cast(shared_from_this());
}
return std::shared_ptr();
}
void ProjectSubClip::setThumbnail(const QImage &img)
{
QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
int duration = m_parentDuration;
- m_outPoint - m_inPoint;
double factor = ((double) thumb.width()) / duration;
int zoneOut = m_outPoint - duration;
QRect zoneRect(0, 0, thumb.width(), thumb.height());
zoneRect.adjust(0, zoneRect.height() * 0.9, 0, -zoneRect.height() * 0.05);
QPainter painter(&thumb);
painter.fillRect(zoneRect, Qt::darkGreen);
zoneRect.adjust(m_inPoint * factor, 0, zoneOut * factor, 0);
painter.fillRect(zoneRect, Qt::green);
painter.end();
m_thumbnail = QIcon(thumb);
if (auto ptr = m_model.lock())
std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()),
AbstractProjectItem::DataThumbnail);
}
QPixmap ProjectSubClip::thumbnail(int width, int height)
{
return m_thumbnail.pixmap(width, height);
}
bool ProjectSubClip::rename(const QString &name, int column)
{
// TODO refac: rework this
Q_UNUSED(column)
if (m_name == name) {
return false;
}
// Rename folder
auto *command = new RenameBinSubClipCommand(pCore->bin(), m_masterClip->clipId(), name, m_name, m_inPoint, m_outPoint);
pCore->currentDoc()->commandStack()->push(command);
return true;
}
std::shared_ptr ProjectSubClip::getMasterClip() const
{
return m_masterClip;
}
ClipType::ProducerType ProjectSubClip::clipType() const
{
return m_masterClip->clipType();
}
bool ProjectSubClip::hasAudioAndVideo() const
{
return m_masterClip->hasAudioAndVideo();
}
void ProjectSubClip::getThumbFromPercent(int percent)
{
// extract a maximum of 50 frames for bin preview
percent += percent%2;
int framePos = (m_outPoint - m_inPoint) * percent / 100;
if (ThumbnailCache::get()->hasThumbnail(m_parentClipId, m_inPoint + framePos)) {
setThumbnail(ThumbnailCache::get()->getThumbnail(m_parentClipId, m_inPoint + framePos));
} else {
// Generate percent thumbs
int id;
if (pCore->jobManager()->hasPendingJob(m_parentClipId, AbstractClipJob::CACHEJOB, &id)) {
} else {
pCore->jobManager()->startJob({m_parentClipId}, -1, QString(), 150, 25, m_inPoint, m_outPoint);
}
}
}
diff --git a/src/jobs/cachejob.cpp b/src/jobs/cachejob.cpp
index b27f1dbf6..f9dd68e7e 100644
--- a/src/jobs/cachejob.cpp
+++ b/src/jobs/cachejob.cpp
@@ -1,116 +1,117 @@
/***************************************************************************
* Copyright (C) 2019 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "cachejob.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "bin/projectsubclip.h"
#include "core.h"
#include "doc/kthumb.h"
#include "klocalizedstring.h"
#include "macros.hpp"
#include "utils/thumbnailcache.hpp"
#include
#include
#include
#include
CacheJob::CacheJob(const QString &binId, int imageHeight, int thumbsCount, int inPoint, int outPoint)
: AbstractClipJob(CACHEJOB, binId)
, m_fullWidth(imageHeight * pCore->getCurrentDar() + 0.5)
, m_imageHeight(imageHeight)
, m_done(false)
, m_thumbsCount(thumbsCount)
, m_inPoint(inPoint)
, m_outPoint(outPoint)
{
if (m_fullWidth % 8 > 0) {
m_fullWidth += 8 - m_fullWidth % 8;
}
m_imageHeight += m_imageHeight % 2;
auto item = pCore->projectItemModel()->getItemByBinId(binId);
Q_ASSERT(item != nullptr && item->itemType() == AbstractProjectItem::ClipItem);
}
const QString CacheJob::getDescription() const
{
return i18n("Extracting thumbs from clip %1", m_clipId);
}
bool CacheJob::startJob()
{
// We reload here, because things may have changed since creation of this job
m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId);
if (m_binClip->clipType() != ClipType::Video && m_binClip->clipType() != ClipType::AV && m_binClip->clipType() != ClipType::Playlist) {
// Don't create thumbnail for audio clips
qDebug()<<"!!!!!!!!!!!\n\n WRONG CLIP TYPE\n\n!!!!!!!!!!";
m_done = false;
return true;
}
m_prod = m_binClip->thumbProducer();
if ((m_prod == nullptr) || !m_prod->is_valid()) {
qDebug() << "********\nCOULD NOT READ THUMB PRODUCER\n********";
return false;
}
int duration = m_outPoint > 0 ? m_outPoint - m_inPoint : m_binClip->frameDuration();
if (m_thumbsCount * 5 > duration) {
m_thumbsCount = duration / 10;
}
std::set frames;
for (int i = 1; i <= m_thumbsCount; ++i) {
frames.insert(m_inPoint + (duration * i / m_thumbsCount));
}
int size = frames.size();
int count = 0;
connect(this, &CacheJob::jobCanceled, [&] () {
+ m_clipId.clear();
m_done = true;
});
for (int i : frames) {
if (m_done) {
break;
}
emit jobProgress(100 * count / size);
count++;
- if (ThumbnailCache::get()->hasThumbnail(m_binClip->clipId(), i)) {
+ if (ThumbnailCache::get()->hasThumbnail(m_clipId, i)) {
continue;
}
m_prod->seek(i);
QScopedPointer frame(m_prod->get_frame());
frame->set("deinterlace_method", "onefield");
frame->set("top_field_first", -1);
frame->set("rescale.interp", "nearest");
if (!m_done && (frame != nullptr) && frame->is_valid()) {
QImage result = KThumb::getFrame(frame.data());
- ThumbnailCache::get()->storeThumbnail(m_binClip->clipId(), i, result, true);
+ ThumbnailCache::get()->storeThumbnail(m_clipId, i, result, true);
}
}
m_done = true;
return true;
}
bool CacheJob::commitResult(Fun &undo, Fun &redo)
{
Q_ASSERT(!m_resultConsumed);
m_resultConsumed = true;
return m_done;
}
diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp
index ba70f2cb1..51df6b66c 100644
--- a/src/mltcontroller/clipcontroller.cpp
+++ b/src/mltcontroller/clipcontroller.cpp
@@ -1,981 +1,1039 @@
/*
Copyright (C) 2012 Till Theato
Copyright (C) 2014 Jean-Baptiste Mardelle
This file is part of Kdenlive. See www.kdenlive.org.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License or (at your option) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "clipcontroller.h"
#include "bin/model/markerlistmodel.hpp"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
#include "lib/audio/audioStreamInfo.h"
#include "profiles/profilemodel.hpp"
#include "core.h"
#include "kdenlive_debug.h"
#include
#include
#include
std::shared_ptr ClipController::mediaUnavailable;
ClipController::ClipController(const QString &clipId, const std::shared_ptr &producer)
: selectedEffectIndex(1)
, m_audioThumbCreated(false)
, m_masterProducer(producer)
, m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr)
, m_usesProxy(false)
, m_audioInfo(nullptr)
, m_videoIndex(0)
, m_clipType(ClipType::Unknown)
, m_hasLimitedDuration(true)
, m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr)
, m_hasAudio(false)
, m_hasVideo(false)
, m_controllerBinId(clipId)
+ , m_producerLock(QReadWriteLock::Recursive)
{
if (m_masterProducer && !m_masterProducer->is_valid()) {
qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER";
return;
}
if (m_properties) {
setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId);
m_service = m_properties->get("mlt_service");
QString proxy = m_properties->get("kdenlive:proxy");
QString path = m_properties->get("resource");
if (proxy.length() > 2) {
if (QFileInfo(path).isRelative()) {
path.prepend(pCore->currentDoc()->documentRoot());
m_properties->set("resource", path.toUtf8().constData());
}
// This is a proxy producer, read original url from kdenlive property
path = m_properties->get("kdenlive:originalurl");
if (QFileInfo(path).isRelative()) {
path.prepend(pCore->currentDoc()->documentRoot());
}
m_usesProxy = true;
} else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative() &&
path != QLatin1String("")) {
path.prepend(pCore->currentDoc()->documentRoot());
m_properties->set("resource", path.toUtf8().constData());
}
m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath();
getInfoForProducer();
checkAudioVideo();
} else {
- m_producerLock.lock();
+ m_producerLock.lockForWrite();
}
}
ClipController::~ClipController()
{
delete m_properties;
m_masterProducer.reset();
}
const QString ClipController::binId() const
{
return m_controllerBinId;
}
const std::unique_ptr &ClipController::audioInfo() const
{
return m_audioInfo;
}
void ClipController::addMasterProducer(const std::shared_ptr &producer)
{
qDebug() << "################### ClipController::addmasterproducer";
QString documentRoot = pCore->currentDoc()->documentRoot();
m_masterProducer = producer;
m_properties = new Mlt::Properties(m_masterProducer->get_properties());
+ m_producerLock.unlock();
+ // Pass temporary properties
+ QMapIterator i(m_tempProps);
+ while (i.hasNext()) {
+ i.next();
+ switch(i.value().type()) {
+ case QVariant::Int:
+ setProducerProperty(i.key(), i.value().toInt());
+ break;
+ case QVariant::Double:
+ setProducerProperty(i.key(), i.value().toDouble());
+ break;
+ default:
+ setProducerProperty(i.key(), i.value().toString());
+ break;
+ }
+ }
+ m_tempProps.clear();
int id = m_controllerBinId.toInt();
m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack());
if (!m_masterProducer->is_valid()) {
m_masterProducer = ClipController::mediaUnavailable;
- m_producerLock.unlock();
qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER";
} else {
checkAudioVideo();
- m_producerLock.unlock();
QString proxy = m_properties->get("kdenlive:proxy");
m_service = m_properties->get("mlt_service");
QString path = m_properties->get("resource");
m_usesProxy = false;
if (proxy.length() > 2) {
// This is a proxy producer, read original url from kdenlive property
path = m_properties->get("kdenlive:originalurl");
if (QFileInfo(path).isRelative()) {
path.prepend(documentRoot);
}
m_usesProxy = true;
} else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative()) {
path.prepend(documentRoot);
}
m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath();
getInfoForProducer();
emitProducerChanged(m_controllerBinId, producer);
setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId);
}
connectEffectStack();
}
namespace {
QString producerXml(const std::shared_ptr &producer, bool includeMeta, bool includeProfile)
{
Mlt::Consumer c(*producer->profile(), "xml", "string");
Mlt::Service s(producer->get_service());
if (!s.is_valid()) {
return QString();
}
int ignore = s.get_int("ignore_points");
if (ignore != 0) {
s.set("ignore_points", 0);
}
c.set("time_format", "frames");
if (!includeMeta) {
c.set("no_meta", 1);
}
if (!includeProfile) {
c.set("no_profile", 1);
}
c.set("store", "kdenlive");
c.set("no_root", 1);
c.set("root", "/");
c.connect(s);
c.start();
if (ignore != 0) {
s.set("ignore_points", ignore);
}
return QString::fromUtf8(c.get("string"));
}
} // namespace
void ClipController::getProducerXML(QDomDocument &document, bool includeMeta, bool includeProfile)
{
// TODO refac this is a probable duplicate with Clip::xml
if (m_masterProducer) {
QString xml = producerXml(m_masterProducer, includeMeta, includeProfile);
document.setContent(xml);
} else {
qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD";
}
}
void ClipController::getInfoForProducer()
{
+ QReadLocker lock(&m_producerLock);
date = QFileInfo(m_path).lastModified();
m_videoIndex = -1;
int audioIndex = -1;
// special case: playlist with a proxy clip have to be detected separately
if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) {
m_clipType = ClipType::Playlist;
} else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) {
audioIndex = getProducerIntProperty(QStringLiteral("audio_index"));
m_videoIndex = getProducerIntProperty(QStringLiteral("video_index"));
if (m_videoIndex == -1) {
m_clipType = ClipType::Audio;
} else {
if (audioIndex == -1) {
m_clipType = ClipType::Video;
} else {
m_clipType = ClipType::AV;
}
if (m_service == QLatin1String("avformat")) {
m_properties->set("mlt_service", "avformat-novalidate");
}
}
} else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) {
if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) {
m_clipType = ClipType::SlideShow;
m_hasLimitedDuration = true;
} else {
m_clipType = ClipType::Image;
m_hasLimitedDuration = false;
}
} else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) {
m_clipType = ClipType::Color;
m_hasLimitedDuration = false;
} else if (m_service == QLatin1String("kdenlivetitle")) {
if (!m_path.isEmpty()) {
m_clipType = ClipType::TextTemplate;
} else {
m_clipType = ClipType::Text;
}
m_hasLimitedDuration = false;
} else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) {
m_clipType = ClipType::Playlist;
} else if (m_service == QLatin1String("webvfx")) {
m_clipType = ClipType::WebVfx;
} else if (m_service == QLatin1String("qtext")) {
m_clipType = ClipType::QText;
} else if (m_service == QLatin1String("blipflash")) {
// Mostly used for testing
m_clipType = ClipType::AV;
m_hasLimitedDuration = true;
} else {
m_clipType = ClipType::Unknown;
}
if (audioIndex > -1 || m_clipType == ClipType::Playlist) {
m_audioInfo = std::make_unique(m_masterProducer, audioIndex);
}
if (!m_hasLimitedDuration) {
int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
if (playtime <= 0) {
// Fix clips having missing kdenlive:duration
m_masterProducer->set("kdenlive:duration", m_masterProducer->frames_to_time(m_masterProducer->get_playtime(), mlt_time_clock));
m_masterProducer->set("out", m_masterProducer->frames_to_time(m_masterProducer->get_length() - 1, mlt_time_clock));
}
}
}
bool ClipController::hasLimitedDuration() const
{
return m_hasLimitedDuration;
}
void ClipController::forceLimitedDuration()
{
m_hasLimitedDuration = true;
}
std::shared_ptr ClipController::originalProducer()
{
- QMutexLocker lock(&m_producerLock);
+ QReadLocker lock(&m_producerLock);
return m_masterProducer;
}
Mlt::Producer *ClipController::masterProducer()
{
return new Mlt::Producer(*m_masterProducer);
}
bool ClipController::isValid()
{
if (m_masterProducer == nullptr) {
return false;
}
return m_masterProducer->is_valid();
}
// static
const char *ClipController::getPassPropertiesList(bool passLength)
{
if (!passLength) {
return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_"
"colorspace,set.force_full_luma,file_hash,autorotate,xmldata,video_index,audio_index,set.test_image,set.test_audio";
}
return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_"
"colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length,video_index,audio_index,set.test_image,set.test_audio";
}
QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix)
{
+ QReadLocker lock(&m_producerLock);
Mlt::Properties subProperties;
subProperties.pass_values(*m_properties, prefix.toUtf8().constData());
QMap subclipsData;
for (int i = 0; i < subProperties.count(); i++) {
subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i));
}
return subclipsData;
}
void ClipController::updateProducer(const std::shared_ptr &producer)
{
qDebug() << "################### ClipController::updateProducer";
// TODO replace all track producers
if (!m_properties) {
// producer has not been initialized
return addMasterProducer(producer);
}
+ m_producerLock.lockForWrite();
Mlt::Properties passProperties;
// Keep track of necessary properties
QString proxy = producer->get("kdenlive:proxy");
if (proxy.length() > 2) {
// This is a proxy producer, read original url from kdenlive property
m_usesProxy = true;
} else {
m_usesProxy = false;
}
// When resetting profile, duration can change so we invalidate it to 0 in that case
int length = m_properties->get_int("length");
const char *passList = getPassPropertiesList(m_usesProxy && length > 0);
// This is necessary as some properties like set.test_audio are reset on producer creation
passProperties.pass_list(*m_properties, passList);
delete m_properties;
*m_masterProducer = producer.get();
- checkAudioVideo();
m_properties = new Mlt::Properties(m_masterProducer->get_properties());
+ m_producerLock.unlock();
+ checkAudioVideo();
// Pass properties from previous producer
m_properties->pass_list(passProperties, passList);
if (!m_masterProducer->is_valid()) {
qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER";
} else {
m_effectStack->resetService(m_masterProducer);
emitProducerChanged(m_controllerBinId, producer);
// URL and name should not be updated otherwise when proxying a clip we cannot find back the original url
/*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource"));
if (m_url.isValid()) {
m_name = m_url.fileName();
}
*/
}
qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource");
}
const QString ClipController::getStringDuration()
{
+ QReadLocker lock(&m_producerLock);
if (m_masterProducer) {
int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
if (playtime > 0) {
return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df));
}
return m_properties->frames_to_time(m_masterProducer->get_length(), mlt_time_smpte_df);
}
return i18n("Unknown");
}
int ClipController::getProducerDuration() const
{
+ QReadLocker lock(&m_producerLock);
if (m_masterProducer) {
int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
if (playtime <= 0) {
return playtime = m_masterProducer->get_length();
}
return playtime;
}
return -1;
}
char *ClipController::framesToTime(int frames) const
{
+ QReadLocker lock(&m_producerLock);
if (m_masterProducer) {
return m_masterProducer->frames_to_time(frames, mlt_time_clock);
}
return nullptr;
}
GenTime ClipController::getPlaytime() const
{
+ QReadLocker lock(&m_producerLock);
if (!m_masterProducer || !m_masterProducer->is_valid()) {
return GenTime();
}
double fps = pCore->getCurrentFps();
if (!m_hasLimitedDuration) {
int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps);
}
return {m_masterProducer->get_playtime(), fps};
}
int ClipController::getFramePlaytime() const
{
+ QReadLocker lock(&m_producerLock);
if (!m_masterProducer || !m_masterProducer->is_valid()) {
return 0;
}
if (!m_hasLimitedDuration) {
int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
return playtime == 0 ? m_masterProducer->get_playtime() : playtime;
}
return m_masterProducer->get_playtime();
}
QString ClipController::getProducerProperty(const QString &name) const
{
- if (!m_properties) {
+ QReadLocker lock(&m_producerLock);
+ if (m_properties == nullptr) {
return QString();
}
if (m_usesProxy && name.startsWith(QLatin1String("meta."))) {
QString correctedName = QStringLiteral("kdenlive:") + name;
return m_properties->get(correctedName.toUtf8().constData());
}
return QString(m_properties->get(name.toUtf8().constData()));
}
int ClipController::getProducerIntProperty(const QString &name) const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return 0;
}
if (m_usesProxy && name.startsWith(QLatin1String("meta."))) {
QString correctedName = QStringLiteral("kdenlive:") + name;
return m_properties->get_int(correctedName.toUtf8().constData());
}
return m_properties->get_int(name.toUtf8().constData());
}
qint64 ClipController::getProducerInt64Property(const QString &name) const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return 0;
}
return m_properties->get_int64(name.toUtf8().constData());
}
double ClipController::getProducerDoubleProperty(const QString &name) const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return 0;
}
return m_properties->get_double(name.toUtf8().constData());
}
QColor ClipController::getProducerColorProperty(const QString &name) const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return {};
}
mlt_color color = m_properties->get_color(name.toUtf8().constData());
return QColor::fromRgb(color.r, color.g, color.b);
}
QMap ClipController::currentProperties(const QMap &props)
{
QMap currentProps;
QMap::const_iterator i = props.constBegin();
while (i != props.constEnd()) {
currentProps.insert(i.key(), getProducerProperty(i.key()));
++i;
}
return currentProps;
}
double ClipController::originalFps() const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return 0;
}
QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex);
return m_properties->get_double(propertyName.toUtf8().constData());
}
QString ClipController::videoCodecProperty(const QString &property) const
{
+ QReadLocker lock(&m_producerLock);
if (!m_properties) {
return QString();
}
QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property);
return m_properties->get(propertyName.toUtf8().constData());
}
const QString ClipController::codec(bool audioCodec) const
{
+ QReadLocker lock(&m_producerLock);
if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) {
return QString();
}
QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_properties->get_int("audio_index") : m_videoIndex);
return m_properties->get(propertyName.toUtf8().constData());
}
const QString ClipController::clipUrl() const
{
return m_path;
}
bool ClipController::sourceExists() const
{
if (m_clipType == ClipType::Color || m_clipType == ClipType::Text) {
return true;
}
if (m_clipType == ClipType::SlideShow) {
//TODO
return true;
}
return QFile::exists(m_path);
}
QString ClipController::clipName() const
{
QString name = getProducerProperty(QStringLiteral("kdenlive:clipname"));
if (!name.isEmpty()) {
return name;
}
return QFileInfo(m_path).fileName();
}
QString ClipController::description() const
{
if (m_clipType == ClipType::TextTemplate) {
QString name = getProducerProperty(QStringLiteral("templatetext"));
return name;
}
QString name = getProducerProperty(QStringLiteral("kdenlive:description"));
if (!name.isEmpty()) {
return name;
}
return getProducerProperty(QStringLiteral("meta.attr.comment.markup"));
}
QString ClipController::serviceName() const
{
return m_service;
}
void ClipController::setProducerProperty(const QString &name, int value)
{
- if (!m_masterProducer) return;
- // TODO: also set property on all track producers
+ if (!m_masterProducer) {
+ m_tempProps.insert(name, value);
+ return;
+ }
+ QWriteLocker lock(&m_producerLock);
m_masterProducer->parent().set(name.toUtf8().constData(), value);
}
void ClipController::setProducerProperty(const QString &name, double value)
{
- if (!m_masterProducer) return;
- // TODO: also set property on all track producers
+ if (!m_masterProducer) {
+ m_tempProps.insert(name, value);
+ return;
+ }
+ QWriteLocker lock(&m_producerLock);
m_masterProducer->parent().set(name.toUtf8().constData(), value);
}
void ClipController::setProducerProperty(const QString &name, const QString &value)
{
- if (!m_masterProducer) return;
- // TODO: also set property on all track producers
+ if (!m_masterProducer) {
+ m_tempProps.insert(name, value);
+ return;
+ }
+
+ QWriteLocker lock(&m_producerLock);
if (value.isEmpty()) {
m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr);
} else {
m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData());
}
}
void ClipController::resetProducerProperty(const QString &name)
{
- // TODO: also set property on all track producers
- if (!m_masterProducer) return;
+ if (!m_masterProducer) {
+ m_tempProps.insert(name, QString());
+ return;
+ }
+
+ QWriteLocker lock(&m_producerLock);
m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr);
}
ClipType::ProducerType ClipController::clipType() const
{
return m_clipType;
}
const QSize ClipController::getFrameSize() const
{
+ QReadLocker lock(&m_producerLock);
if (m_masterProducer == nullptr) {
return QSize();
}
int width = m_masterProducer->get_int("meta.media.width");
if (width == 0) {
width = m_masterProducer->get_int("width");
}
int height = m_masterProducer->get_int("meta.media.height");
if (height == 0) {
height = m_masterProducer->get_int("height");
}
return QSize(width, height);
}
bool ClipController::hasAudio() const
{
return m_hasAudio;
}
void ClipController::checkAudioVideo()
{
+ QReadLocker lock(&m_producerLock);
m_masterProducer->seek(0);
if (m_masterProducer->get_int("_placeholder") == 1 || m_masterProducer->get("text") == QLatin1String("INVALID")) {
// This is a placeholder file, try to guess from its properties
QString orig_service = m_masterProducer->get("kdenlive:orig_service");
if (orig_service.startsWith(QStringLiteral("avformat")) || (m_masterProducer->get_int("audio_index") + m_masterProducer->get_int("video_index") > 0)) {
m_hasAudio = m_masterProducer->get_int("audio_index") >= 0;
m_hasVideo = m_masterProducer->get_int("video_index") >= 0;
} else {
// Assume image or text producer
m_hasAudio = false;
m_hasVideo = true;
}
return;
}
QScopedPointer frame(m_masterProducer->get_frame());
if (frame->is_valid()) {
// test_audio returns 1 if there is NO audio (strange but true at the time this code is written)
m_hasAudio = frame->get_int("test_audio") == 0;
m_hasVideo = frame->get_int("test_image") == 0;
} else {
qDebug()<<"* * * *ERROR INVALID FRAME On test";
}
}
bool ClipController::hasVideo() const
{
return m_hasVideo;
}
PlaylistState::ClipState ClipController::defaultState() const
{
if (hasVideo()) {
return PlaylistState::VideoOnly;
}
if (hasAudio()) {
return PlaylistState::AudioOnly;
}
return PlaylistState::Disabled;
}
QPixmap ClipController::pixmap(int framePosition, int width, int height)
{
// TODO refac this should use the new thumb infrastructure
+ QReadLocker lock(&m_producerLock);
m_masterProducer->seek(framePosition);
Mlt::Frame *frame = m_masterProducer->get_frame();
if (frame == nullptr || !frame->is_valid()) {
QPixmap p(width, height);
p.fill(QColor(Qt::red).rgb());
return p;
}
frame->set("rescale.interp", "bilinear");
frame->set("deinterlace_method", "onefield");
frame->set("top_field_first", -1);
if (width == 0) {
width = m_masterProducer->get_int("meta.media.width");
if (width == 0) {
width = m_masterProducer->get_int("width");
}
}
if (height == 0) {
height = m_masterProducer->get_int("meta.media.height");
if (height == 0) {
height = m_masterProducer->get_int("height");
}
}
// int ow = frameWidth;
// int oh = height;
mlt_image_format format = mlt_image_rgb24a;
width += width % 2;
height += height % 2;
const uchar *imagedata = frame->get_image(format, width, height);
QImage image(imagedata, width, height, QImage::Format_RGBA8888);
QPixmap pixmap;
pixmap.convertFromImage(image);
delete frame;
return pixmap;
}
void ClipController::setZone(const QPoint &zone)
{
setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x());
setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y());
}
QPoint ClipController::zone() const
{
int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in"));
int max = getFramePlaytime() - 1;
int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max);
if (out <= in) {
out = max;
}
QPoint zone(in, out);
return zone;
}
const QString ClipController::getClipHash() const
{
return getProducerProperty(QStringLiteral("kdenlive:file_hash"));
}
Mlt::Properties &ClipController::properties()
{
+ QReadLocker lock(&m_producerLock);
return *m_properties;
}
void ClipController::backupOriginalProperties()
{
+ QReadLocker lock(&m_producerLock);
if (m_properties->get_int("kdenlive:original.backup") == 1) {
return;
}
int propsCount = m_properties->count();
// store original props
QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")};
for (int j = 0; j < propsCount; j++) {
QString propName = m_properties->get_name(j);
if (doNotPass.contains(propName)) {
continue;
}
if (!propName.startsWith(QLatin1Char('_'))) {
propName.prepend(QStringLiteral("kdenlive:original."));
m_properties->set(propName.toUtf8().constData(), m_properties->get(j));
}
}
m_properties->set("kdenlive:original.backup", 1);
}
void ClipController::clearBackupProperties()
{
+ QReadLocker lock(&m_producerLock);
if (m_properties->get_int("kdenlive:original.backup") == 0) {
return;
}
int propsCount = m_properties->count();
// clear original props
QStringList passProps;
for (int j = 0; j < propsCount; j++) {
QString propName = m_properties->get_name(j);
if (propName.startsWith(QLatin1String("kdenlive:original."))) {
passProps << propName;
}
}
for (const QString &p : passProps) {
m_properties->set(p.toUtf8().constData(), (char *)nullptr);
}
m_properties->set("kdenlive:original.backup", (char *)nullptr);
}
void ClipController::mirrorOriginalProperties(Mlt::Properties &props)
{
+ QReadLocker lock(&m_producerLock);
if (m_usesProxy && QFileInfo(m_properties->get("resource")).fileName() == QFileInfo(m_properties->get("kdenlive:proxy")).fileName()) {
// This is a proxy, we need to use the real source properties
if (m_properties->get_int("kdenlive:original.backup") == 0) {
// We have a proxy clip, load original source producer
std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData());
// Get frame to make sure we retrieve all original props
std::shared_ptr fr(prod->get_frame());
if (!prod->is_valid()) {
return;
}
int width = 0;
int height = 0;
mlt_image_format format = mlt_image_none;
fr->get_image(format, width, height);
Mlt::Properties sourceProps(prod->get_properties());
props.inherit(sourceProps);
int propsCount = sourceProps.count();
// store original props
QStringList doNotPass {QStringLiteral("kdenlive:proxy"),QStringLiteral("kdenlive:originalurl"),QStringLiteral("kdenlive:clipname")};
for (int i = 0; i < propsCount; i++) {
QString propName = sourceProps.get_name(i);
if (doNotPass.contains(propName)) {
continue;
}
if (!propName.startsWith(QLatin1Char('_'))) {
propName.prepend(QStringLiteral("kdenlive:original."));
m_properties->set(propName.toUtf8().constData(), sourceProps.get(i));
}
}
m_properties->set("kdenlive:original.backup", 1);
}
// Properties were fetched in the past, reuse
Mlt::Properties sourceProps;
sourceProps.pass_values(*m_properties, "kdenlive:original.");
props.inherit(sourceProps);
} else {
if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio) {
// Make sure that a frame / image was fetched to initialize all meta properties
QString progressive = m_properties->get("meta.media.progressive");
if (progressive.isEmpty()) {
// Fetch a frame to initialize required properties
QScopedPointer tmpProd(nullptr);
if (KdenliveSettings::gpu_accel()) {
QString service = m_masterProducer->get("mlt_service");
tmpProd.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), m_masterProducer->get("resource")));
}
std::shared_ptr fr(tmpProd ? tmpProd->get_frame() : m_masterProducer->get_frame());
mlt_image_format format = mlt_image_none;
int width = 0;
int height = 0;
fr->get_image(format, width, height);
}
}
props.inherit(*m_properties);
}
}
void ClipController::addEffect(QDomElement &xml)
{
Q_UNUSED(xml)
// TODO refac: this must be rewritten
/*
QMutexLocker lock(&m_effectMutex);
Mlt::Service service = m_masterProducer->parent();
ItemInfo info;
info.cropStart = GenTime();
info.cropDuration = getPlaytime();
EffectsList eff = effectList();
EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml);
// Add effect to list and setup a kdenlive_ix value
int kdenlive_ix = 0;
for (int i = 0; i < service.filter_count(); ++i) {
QScopedPointer effect(service.filter(i));
int ix = effect->get_int("kdenlive_ix");
if (ix > kdenlive_ix) {
kdenlive_ix = ix;
}
}
kdenlive_ix++;
xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix);
EffectsParameterList params = EffectsController::getEffectArgs(xml);
EffectManager effect(service);
effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps()));
if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId);
*/
}
void ClipController::removeEffect(int effectIndex, bool delayRefresh)
{
Q_UNUSED(effectIndex) Q_UNUSED(delayRefresh)
// TODO refac: this must be rewritten
/*
QMutexLocker lock(&m_effectMutex);
Mlt::Service service(m_masterProducer->parent());
EffectManager effect(service);
effect.removeEffect(effectIndex, true);
if (!delayRefresh) {
if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId);
}
*/
}
void ClipController::moveEffect(int oldPos, int newPos)
{
Q_UNUSED(oldPos)
Q_UNUSED(newPos)
// TODO refac: this must be rewritten
/*
QMutexLocker lock(&m_effectMutex);
Mlt::Service service(m_masterProducer->parent());
EffectManager effect(service);
effect.moveEffect(oldPos, newPos);
*/
}
int ClipController::effectsCount()
{
int count = 0;
+ QReadLocker lock(&m_producerLock);
Mlt::Service service(m_masterProducer->parent());
for (int ix = 0; ix < service.filter_count(); ++ix) {
QScopedPointer effect(service.filter(ix));
QString id = effect->get("kdenlive_id");
if (!id.isEmpty()) {
count++;
}
}
return count;
}
void ClipController::changeEffectState(const QList &indexes, bool disable)
{
Q_UNUSED(indexes)
Q_UNUSED(disable)
// TODO refac : this must be rewritten
/*
Mlt::Service service = m_masterProducer->parent();
for (int i = 0; i < service.filter_count(); ++i) {
QScopedPointer effect(service.filter(i));
if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) {
effect->set("disable", (int)disable);
}
}
if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId);
*/
}
void ClipController::updateEffect(const QDomElement &e, int ix)
{
Q_UNUSED(e)
Q_UNUSED(ix)
// TODO refac : this must be rewritten
/*
QString tag = e.attribute(QStringLiteral("id"));
if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) {
// this filters cannot be edited, remove and re-add it
removeEffect(ix, true);
QDomElement clone = e.cloneNode().toElement();
addEffect(clone);
return;
}
EffectsParameterList params = EffectsController::getEffectArgs(e);
Mlt::Service service = m_masterProducer->parent();
for (int i = 0; i < service.filter_count(); ++i) {
QScopedPointer effect(service.filter(i));
if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) {
continue;
}
service.lock();
QString prefix;
QString ser = effect->get("mlt_service");
if (ser == QLatin1String("region")) {
prefix = QStringLiteral("filter0.");
}
for (int j = 0; j < params.count(); ++j) {
effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData());
// qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId);
// slotRefreshTracks();
*/
}
bool ClipController::hasEffects() const
{
return m_effectStack->rowCount() > 0;
}
void ClipController::setBinEffectsEnabled(bool enabled)
{
m_effectStack->setEffectStackEnabled(enabled);
}
void ClipController::saveZone(QPoint zone, const QDir &dir)
{
QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt"));
if (dir.exists(path)) {
// TODO ask for overwrite
}
Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData());
xmlConsumer.set("terminate_on_pause", 1);
+ QReadLocker lock(&m_producerLock);
Mlt::Producer prod(m_masterProducer->get_producer());
Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y());
Mlt::Playlist list(pCore->getCurrentProfile()->profile());
list.insert_at(0, *prod2, 0);
// list.set("title", desc.toUtf8().constData());
xmlConsumer.connect(list);
xmlConsumer.run();
delete prod2;
}
std::shared_ptr ClipController::getEffectStack() const
{
return m_effectStack;
}
bool ClipController::addEffect(const QString &effectId)
{
return m_effectStack->appendEffect(effectId, true);
}
bool ClipController::copyEffect(const std::shared_ptr &stackModel, int rowId)
{
m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId),
!m_hasAudio ? PlaylistState::VideoOnly : !m_hasVideo ? PlaylistState::AudioOnly : PlaylistState::Disabled);
return true;
}
std::shared_ptr ClipController::getMarkerModel() const
{
return m_markerModel;
}
void ClipController::refreshAudioInfo()
{
if (m_audioInfo && m_masterProducer) {
+ QReadLocker lock(&m_producerLock);
m_audioInfo->setAudioIndex(m_masterProducer, m_properties->get_int("audio_index"));
}
}
diff --git a/src/mltcontroller/clipcontroller.h b/src/mltcontroller/clipcontroller.h
index 35fe4c8ac..1b047b423 100644
--- a/src/mltcontroller/clipcontroller.h
+++ b/src/mltcontroller/clipcontroller.h
@@ -1,239 +1,243 @@
/*
Copyright (C) 2012 Till Theato
Copyright (C) 2014 Jean-Baptiste Mardelle
This file is part of Kdenlive. See www.kdenlive.org.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License or (at your option) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#ifndef CLIPCONTROLLER_H
#define CLIPCONTROLLER_H
#include "definitions.h"
#include
#include
#include
#include
+#include
#include
#include
class QPixmap;
class Bin;
class AudioStreamInfo;
class EffectStackModel;
class MarkerListModel;
/**
* @class ClipController
* @brief Provides a convenience wrapper around the project Bin clip producers.
* It also holds a QList of track producers for the 'master' producer in case we
* need to update or replace them
*/
class ClipController
{
public:
friend class Bin;
/**
* @brief Constructor.
The constructor is protected because you should call the static Construct instead
* @param bincontroller reference to the bincontroller
* @param producer producer to create reference to
*/
explicit ClipController(const QString &id, const std::shared_ptr &producer = nullptr);
public:
virtual ~ClipController();
QMutex producerMutex;
/** @brief Returns true if the master producer is valid */
bool isValid();
-
+
/** @brief Returns true if the source file exists */
bool sourceExists() const;
/** @brief Stores the file's creation time */
QDateTime date;
/** @brief Replaces the master producer and (TODO) the track producers with an updated producer, for example a proxy */
void updateProducer(const std::shared_ptr &producer);
void getProducerXML(QDomDocument &document, bool includeMeta = false, bool includeProfile = true);
/** @brief Returns a clone of our master producer. Delete after use! */
Mlt::Producer *masterProducer();
/** @brief Returns the clip name (usually file name) */
QString clipName() const;
/** @brief Returns the clip's description or metadata comment */
QString description() const;
/** @brief Returns the clip's MLT resource */
const QString clipUrl() const;
/** @brief Returns the clip's type as defined in definitions.h */
ClipType::ProducerType clipType() const;
/** @brief Returns the MLT's producer id */
const QString binId() const;
/** @brief Returns the clip's duration */
GenTime getPlaytime() const;
int getFramePlaytime() const;
/**
* @brief Sets a property.
* @param name name of the property
* @param value the new value
*/
void setProducerProperty(const QString &name, const QString &value);
void setProducerProperty(const QString &name, int value);
void setProducerProperty(const QString &name, double value);
/** @brief Reset a property on the MLT producer (=delete the property). */
void resetProducerProperty(const QString &name);
/**
* @brief Returns the list of all properties starting with prefix. For subclips, the list is of this type:
* { subclip name , subclip in/out } where the subclip in/ou value is a semi-colon separated in/out value, like "25;220"
*/
QMap getPropertiesFromPrefix(const QString &prefix, bool withPrefix = false);
/**
* @brief Returns the value of a property.
* @param name name o the property
*/
QMap currentProperties(const QMap &props);
QString getProducerProperty(const QString &key) const;
int getProducerIntProperty(const QString &key) const;
qint64 getProducerInt64Property(const QString &key) const;
QColor getProducerColorProperty(const QString &key) const;
double getProducerDoubleProperty(const QString &key) const;
double originalFps() const;
QString videoCodecProperty(const QString &property) const;
const QString codec(bool audioCodec) const;
const QString getClipHash() const;
const QSize getFrameSize() const;
/** @brief Returns the clip duration as a string like 00:00:02:01. */
const QString getStringDuration();
int getProducerDuration() const;
char *framesToTime(int frames) const;
/**
* @brief Returns a pixmap created from a frame of the producer.
* @param position frame position
* @param width width of the pixmap (only a guidance)
* @param height height of the pixmap (only a guidance)
*/
QPixmap pixmap(int position = 0, int width = 0, int height = 0);
/** @brief Returns the MLT producer's service. */
QString serviceName() const;
/** @brief Returns the original master producer. */
std::shared_ptr originalProducer();
/** @brief Holds index of currently selected master clip effect. */
int selectedEffectIndex;
/** @brief Sets the master producer for this clip when we build the controller without master clip. */
void addMasterProducer(const std::shared_ptr &producer);
/* @brief Returns the marker model associated with this clip */
std::shared_ptr getMarkerModel() const;
void setZone(const QPoint &zone);
QPoint zone() const;
bool hasLimitedDuration() const;
void forceLimitedDuration();
Mlt::Properties &properties();
void mirrorOriginalProperties(Mlt::Properties &props);
void addEffect(QDomElement &xml);
bool copyEffect(const std::shared_ptr &stackModel, int rowId);
void removeEffect(int effectIndex, bool delayRefresh = false);
/** @brief Enable/disable an effect. */
void changeEffectState(const QList &indexes, bool disable);
void updateEffect(const QDomElement &e, int ix);
/** @brief Returns true if the bin clip has effects */
bool hasEffects() const;
/** @brief Returns true if the clip contains at least one audio stream */
bool hasAudio() const;
/** @brief Returns true if the clip contains at least one video stream */
bool hasVideo() const;
/** @brief Returns the default state a clip should be in. If the clips contains both video and audio, this defaults to video */
PlaylistState::ClipState defaultState() const;
/** @brief Returns info about clip audio */
const std::unique_ptr &audioInfo() const;
/** @brief Returns true if audio thumbnails for this clip are cached */
bool m_audioThumbCreated;
/** @brief When replacing a producer, it is important that we keep some properties, for example force_ stuff and url for proxies
* this method returns a list of properties that we want to keep when replacing a producer . */
static const char *getPassPropertiesList(bool passLength = true);
/** @brief Disable all Kdenlive effects on this clip */
void setBinEffectsEnabled(bool enabled);
/** @brief Returns the number of Kdenlive added effects for this bin clip */
int effectsCount();
/** @brief Move an effect in stack for this bin clip */
void moveEffect(int oldPos, int newPos);
/** @brief Save an xml playlist of current clip with in/out points as zone.x()/y() */
void saveZone(QPoint zone, const QDir &dir);
/* @brief This is the producer that serves as a placeholder while a clip is being loaded. It is created in Core at startup */
static std::shared_ptr mediaUnavailable;
/** @brief Returns a ptr to the effetstack associated with this element */
std::shared_ptr getEffectStack() const;
/** @brief Append an effect to this producer's effect list */
bool addEffect(const QString &effectId);
protected:
virtual void emitProducerChanged(const QString & /*unused*/, const std::shared_ptr & /*unused*/){};
virtual void connectEffectStack(){};
// This is the helper function that checks if the clip has audio and video and stores the result
void checkAudioVideo();
// Update audio stream info
void refreshAudioInfo();
void backupOriginalProperties();
void clearBackupProperties();
std::shared_ptr m_masterProducer;
Mlt::Properties *m_properties;
bool m_usesProxy;
std::unique_ptr m_audioInfo;
QString m_service;
QString m_path;
int m_videoIndex;
ClipType::ProducerType m_clipType;
bool m_hasLimitedDuration;
QMutex m_effectMutex;
void getInfoForProducer();
// void rebuildEffectList(ProfileInfo info);
std::shared_ptr m_effectStack;
std::shared_ptr m_markerModel;
bool m_hasAudio;
bool m_hasVideo;
private:
- QMutex m_producerLock;
+ /** @brief Mutex to protect the producer properties on read/write */
+ mutable QReadWriteLock m_producerLock;
+ /** @brief Temporarily store clip properties until producer is available */
+ QMap m_tempProps;
QString m_controllerBinId;
};
#endif
diff --git a/src/timeline2/view/qml/TrackHead.qml b/src/timeline2/view/qml/TrackHead.qml
index faf4d92cf..5dad55902 100644
--- a/src/timeline2/view/qml/TrackHead.qml
+++ b/src/timeline2/view/qml/TrackHead.qml
@@ -1,518 +1,519 @@
/*
* Copyright (c) 2013-2016 Meltytech, LLC
* Author: Dan Dennedy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import QtQuick 2.6
import QtQuick.Controls 1.4
import QtQuick.Controls 2.2 as NEWQML
import QtQuick.Controls.Styles 1.2
import QtQuick.Layouts 1.3
Rectangle {
id: trackHeadRoot
property string trackName
property string effectNames
property bool isStackEnabled
property bool isDisabled
property bool collapsed: false
property int isComposite
property bool isLocked: false
property bool isActive: false
property bool isAudio
property bool showAudioRecord
property bool current: false
property int myTrackHeight
property int trackId : -42
property int collapsedHeight: nameEdit.height + 2
property int iconSize: root.baseUnit * 2
property string trackTag
property int thumbsFormat: 0
border.width: 1
border.color: root.frameColor
signal clicked()
function pulseLockButton() {
flashLock.restart();
}
color: getTrackColor(isAudio, true)
//border.color: selected? 'red' : 'transparent'
//border.width: selected? 1 : 0
clip: true
state: 'normal'
states: [
State {
name: 'current'
when: trackHeadRoot.current
PropertyChanges {
target: trackHeadRoot
color: selectedTrackColor
}
},
State {
when: !trackHeadRoot.current
name: 'normal'
PropertyChanges {
target: trackHeadRoot
color: getTrackColor(isAudio, true)
}
}
]
Keys.onDownPressed: {
root.moveSelectedTrack(1)
}
Keys.onUpPressed: {
root.moveSelectedTrack(-1)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: {
parent.clicked()
if (mouse.button == Qt.RightButton) {
headerMenu.trackId = trackId
headerMenu.thumbsFormat = thumbsFormat
headerMenu.audioTrack = trackHeadRoot.isAudio
headerMenu.recEnabled = trackHeadRoot.showAudioRecord
headerMenu.popup()
}
}
onClicked: {
parent.forceActiveFocus()
nameEdit.visible = false
if (mouse.button == Qt.LeftButton) {
timeline.showTrackAsset(trackId)
}
}
}
ColumnLayout {
id: targetColumn
width: root.baseUnit / 1.3
height: trackHeadRoot.height
Item {
width: parent.width
Layout.fillHeight: true
Layout.topMargin: 1
Layout.bottomMargin: 1
Layout.leftMargin: 1
Layout.alignment: Qt.AlignVCenter
Rectangle {
id: trackTarget
color: 'grey'
anchors.fill: parent
width: height
border.width: 0
visible: trackHeadRoot.isAudio ? timeline.hasAudioTarget : timeline.hasVideoTarget
MouseArea {
id: targetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (trackHeadRoot.isAudio) {
if (trackHeadRoot.trackId == timeline.audioTarget) {
timeline.audioTarget = -1;
} else if (timeline.hasAudioTarget) {
timeline.audioTarget = trackHeadRoot.trackId;
}
} else {
if (trackHeadRoot.trackId == timeline.videoTarget) {
timeline.videoTarget = -1;
} else if (timeline.hasVideoTarget) {
timeline.videoTarget = trackHeadRoot.trackId;
}
}
}
}
NEWQML.ToolTip {
visible: targetArea.containsMouse
font.pixelSize: root.baseUnit
delay: 1500
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: i18n("Click to toggle track as target. Target tracks will receive the inserted clips")
}
}
state: 'normalTarget'
states: [
State {
name: 'target'
when: (trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.audioTarget) || (!trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.videoTarget)
PropertyChanges {
target: trackTarget
color: 'green'
}
},
State {
name: 'inactiveTarget'
when: (trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.lastAudioTarget) || (!trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.lastVideoTarget)
PropertyChanges {
target: trackTarget
opacity: 0.3
color: activePalette.text
}
},
State {
name: 'noTarget'
when: !trackHeadRoot.isLocked && !trackHeadRoot.isDisabled
PropertyChanges {
target: trackTarget
color: activePalette.base
}
}
]
transitions: [
Transition {
to: '*'
ColorAnimation { target: trackTarget; duration: 300 }
}
]
}
}
}
ColumnLayout {
id: trackHeadColumn
spacing: 0
anchors.fill: parent
anchors.leftMargin: targetColumn.width
anchors.topMargin: 0
RowLayout {
spacing: 0
Layout.leftMargin: 2
ToolButton {
id: expandButton
implicitHeight: root.baseUnit * 2
implicitWidth: root.baseUnit * 2
iconName: trackHeadRoot.collapsed ? 'arrow-right' : 'arrow-down'
onClicked: {
trackHeadRoot.myTrackHeight = trackHeadRoot.collapsed ? Math.max(collapsedHeight * 1.5, controller.getTrackProperty(trackId, "kdenlive:trackheight")) : collapsedHeight
}
tooltip: trackLabel.visible? i18n("Minimize") : i18n("Expand")
}
Item {
width: trackTag.contentWidth + 4
height: width
Layout.topMargin: 1
Rectangle {
id: trackLed
color: Qt.darker(trackHeadRoot.color, 0.45)
anchors.fill: parent
width: height
border.width: 0
radius: 2
Text {
id: trackTag
text: trackHeadRoot.trackTag
anchors.fill: parent
font.pixelSize: root.baseUnit * 1.5
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: tagMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
timeline.switchTrackActive(trackHeadRoot.trackId)
}
}
NEWQML.ToolTip {
visible: tagMouseArea.containsMouse
font.pixelSize: root.baseUnit
delay: 1500
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: i18n("Click to make track active/inactive. Active tracks will react to editing operations")
}
}
state: 'normalled'
states: [
State {
name: 'locked'
when: trackHeadRoot.isLocked
PropertyChanges {
target: trackLed
color: 'red'
}
},
State {
name: 'active'
when: trackHeadRoot.isActive
PropertyChanges {
target: trackLed
color: 'yellow'
}
},
State {
name: 'mute'
when: trackHeadRoot.isDisabled
PropertyChanges {
target: trackLed
color: 'orange'
}
},
State {
name: 'inactive'
when: !trackHeadRoot.isLocked && !trackHeadRoot.isActive
PropertyChanges {
target: trackLed
color: Qt.darker(trackHeadRoot.color, 0.45)
}
}
]
transitions: [
Transition {
to: '*'
ColorAnimation { target: trackLed; duration: 300 }
}
]
}
}
Item {
// Spacer
Layout.fillWidth: true
}
ToolButton {
iconName: 'tools-wizard'
checkable: true
enabled: trackHeadRoot.effectNames != ''
checked: enabled && trackHeadRoot.isStackEnabled
implicitHeight: trackHeadRoot.iconSize
implicitWidth: trackHeadRoot.iconSize
onClicked: {
timeline.showTrackAsset(trackId)
controller.setTrackStackEnabled(trackId, !isStackEnabled)
}
}
ToolButton {
id: muteButton
implicitHeight: trackHeadRoot.iconSize
implicitWidth: trackHeadRoot.iconSize
iconName: isAudio ? (isDisabled ? 'kdenlive-hide-audio' : 'kdenlive-show-audio') : (isDisabled ? 'kdenlive-hide-video' : 'kdenlive-show-video')
iconSource: isAudio ? (isDisabled ? 'qrc:///pics/kdenlive-hide-audio.svgz' : 'qrc:///pics/kdenlive-show-audio.svgz') : (isDisabled ? 'qrc:///pics/kdenlive-hide-video.svgz' : 'qrc:///pics/kdenlive-show-video.svgz')
onClicked: controller.setTrackProperty(trackId, "hide", isDisabled ? (isAudio ? '1' : '2') : '3')
tooltip: isAudio ? (isDisabled? i18n("Unmute") : i18n("Mute")) : (isDisabled? i18n("Show") : i18n("Hide"))
}
ToolButton {
id: lockButton
implicitHeight: trackHeadRoot.iconSize
implicitWidth: trackHeadRoot.iconSize
iconName: isLocked ? 'kdenlive-lock' : 'kdenlive-unlock'
iconSource: isLocked ? 'qrc:///pics/kdenlive-lock.svg' : 'qrc:///pics/kdenlive-unlock.svg'
onClicked: controller.setTrackLockedState(trackId, !isLocked)
tooltip: isLocked? i18n("Unlock track") : i18n("Lock track")
SequentialAnimation {
id: flashLock
loops: 1
ScaleAnimator {
target: lockButton
from: 1
to: 2
duration: 500
}
ScaleAnimator {
target: lockButton
from: 2
to: 1
duration: 500
}
}
}
Layout.rightMargin: 4
}
RowLayout {
id: recLayout
Layout.maximumHeight: showAudioRecord ? -1 : 0
Loader {
id: audioVuMeter
Layout.fillWidth: true
Layout.rightMargin: 2
Layout.leftMargin: 4
visible: showAudioRecord && (trackHeadRoot.height >= 2 * muteButton.height + resizer.height)
source: isAudio && showAudioRecord ? "AudioLevels.qml" : ""
onLoaded: item.trackId = trackId
}
}
RowLayout {
Rectangle {
id: trackLabel
color: 'transparent'
Layout.fillWidth: true
radius: 2
border.color: trackNameMouseArea.containsMouse ? activePalette.highlight : 'transparent'
height: nameEdit.height
visible: (trackHeadRoot.height >= trackLabel.height + muteButton.height + resizer.height + recLayout.height)
MouseArea {
id: trackNameMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onDoubleClicked: {
nameEdit.visible = true
nameEdit.focus = true
nameEdit.selectAll()
}
onClicked: {
+ timeline.showTrackAsset(trackId)
trackHeadRoot.clicked()
trackHeadRoot.focus = true
}
onEntered: {
if (nameEdit.visible == false && trackName == '') {
placeHolder.visible = true
}
}
onExited: {
if (placeHolder.visible == true) {
placeHolder.visible = false
}
}
}
Label {
text: trackName
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
elide: Qt.ElideRight
font.pointSize: root.baseUnit * 0.9
}
Label {
id: placeHolder
visible: false
enabled: false
text: i18n("Edit track name")
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
elide: Qt.ElideRight
font.pointSize: root.baseUnit * 0.9
}
TextField {
id: nameEdit
visible: false
width: parent.width
text: trackName
font.pointSize: root.baseUnit * 0.9
style: TextFieldStyle {
padding.top:0
padding.bottom: 0
background: Rectangle {
color: activePalette.base
anchors.fill: parent
}
}
onEditingFinished: {
controller.setTrackProperty(trackId, "kdenlive:track_name", text)
visible = false
}
}
}
}
Item {
// Spacer
id: spacer
Layout.fillWidth: true
Layout.fillHeight: true
}
}
Rectangle {
id: resizer
height: 4
color: 'red'
opacity: 0
Drag.active: trimInMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
width: trackHeadRoot.width
y: trackHeadRoot.height - height
MouseArea {
id: trimInMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeVerCursor
drag.target: parent
drag.axis: Drag.YAxis
drag.minimumY: trackHeadRoot.collapsedHeight - resizer.height
property double startY
property double originalY
drag.smoothed: false
onPressed: {
root.autoScrolling = false
startY = mapToItem(null, x, y).y
originalY = trackHeadRoot.height // reusing originalX to accumulate delta for bubble help
}
onReleased: {
root.autoScrolling = timeline.autoScroll
if (!trimInMouseArea.containsMouse) {
parent.opacity = 0
}
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.adjustAllTrackHeight(trackHeadRoot.trackId, trackHeadRoot.myTrackHeight)
}
}
onEntered: parent.opacity = 0.3
onExited: parent.opacity = 0
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
parent.opacity = 0.5
var newHeight = originalY + (mapToItem(null, x, y).y - startY)
newHeight = Math.max(collapsedHeight, newHeight)
trackHeadRoot.myTrackHeight = newHeight
}
}
}
}
DropArea { //Drop area for tracks
anchors.fill: trackHeadRoot
keys: 'kdenlive/effect'
property string dropData
property string dropSource
property int dropRow: -1
onEntered: {
dropData = drag.getDataAsString('kdenlive/effect')
dropSource = drag.getDataAsString('kdenlive/effectsource')
}
onDropped: {
console.log("Add effect: ", dropData)
if (dropSource == '') {
// drop from effects list
controller.addTrackEffect(trackHeadRoot.trackId, dropData);
} else {
controller.copyTrackEffect(trackHeadRoot.trackId, dropSource);
}
dropSource = ''
dropRow = -1
drag.acceptProposedAction
}
}
}
diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml
index 1047e51ec..86b00501f 100644
--- a/src/timeline2/view/qml/timeline.qml
+++ b/src/timeline2/view/qml/timeline.qml
@@ -1,1517 +1,1516 @@
import QtQuick 2.6
import QtQml.Models 2.2
import QtQuick.Controls 1.4 as OLD
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import Kdenlive.Controls 1.0
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
Rectangle {
id: root
objectName: "timelineview"
SystemPalette { id: activePalette }
color: activePalette.window
property bool validMenu: false
property color textColor: activePalette.text
property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active
signal clipClicked()
signal mousePosChanged(int position)
signal zoomIn(bool onMouse)
signal zoomOut(bool onMouse)
signal processingDrag(bool dragging)
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
ClipMenu {
id: clipMenu
}
CompositionMenu {
id: compositionMenu
}
onDragInProgressChanged: {
processingDrag(!root.dragInProgress)
}
function fitZoom() {
return scrollView.width / (timeline.duration * 1.1)
}
function scrollPos() {
return scrollView.flickableItem.contentX
}
function goToStart(pos) {
scrollView.flickableItem.contentX = pos
}
function updatePalette() {
root.color = activePalette.window
root.textColor = activePalette.text
playhead.fillColor = activePalette.windowText
ruler.repaintRuler()
}
function moveSelectedTrack(offset) {
var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
var newTrack = cTrack + offset
var max = tracksRepeater.count;
if (newTrack < 0) {
newTrack = max - 1;
} else if (newTrack >= max) {
newTrack = 0;
}
console.log('Setting curr tk: ', newTrack, 'MAX: ',max)
timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
}
function zoomByWheel(wheel) {
if (wheel.modifiers & Qt.AltModifier) {
// Seek to next snap
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else if (wheel.modifiers & Qt.ControlModifier) {
root.wheelAccumulatedDelta += wheel.angleDelta.y;
// Zoom
if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
root.zoomIn(true);
root.wheelAccumulatedDelta = 0;
} else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
root.zoomOut(true);
root.wheelAccumulatedDelta = 0;
}
} else if (wheel.modifiers & Qt.ShiftModifier) {
// Vertical scroll
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, trackHeaders.height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
} else {
// Horizontal scroll
var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
}
wheel.accepted = true
}
function continuousScrolling(x) {
// This provides continuous scrolling at the left/right edges.
if (x > scrollView.flickableItem.contentX + scrollView.width - 50) {
scrollTimer.item = clip
scrollTimer.backwards = false
scrollTimer.start()
} else if (x < 50) {
scrollView.flickableItem.contentX = 0;
scrollTimer.stop()
} else if (x < scrollView.flickableItem.contentX + 50) {
scrollTimer.item = clip
scrollTimer.backwards = true
scrollTimer.start()
} else {
scrollTimer.stop()
}
}
function getTrackYFromId(a_track) {
return Logic.getTrackYFromId(a_track)
}
function getTrackYFromMltIndex(a_track) {
return Logic.getTrackYFromMltIndex(a_track)
}
function getTracksCount() {
return Logic.getTracksList()
}
function getMousePos() {
return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
}
function getScrollPos() {
return scrollView.flickableItem.contentX
}
function setScrollPos(pos) {
return scrollView.flickableItem.contentX = pos
}
function getCopiedItemId() {
return copiedClip
}
function getMouseTrack() {
return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
}
function getTrackColor(audio, header) {
var col = activePalette.alternateBase
if (audio) {
col = Qt.tint(col, "#06FF00CC")
}
if (header) {
col = Qt.darker(col, 1.05)
}
return col
}
function clearDropData() {
clipBeingDroppedId = -1
droppedPosition = -1
droppedTrack = -1
scrollTimer.running = false
scrollTimer.stop()
}
function isDragging() {
return dragInProgress
}
function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
dragProxy.x = itemObject.modelStart * timeScale
dragProxy.y = itemCoord.y
dragProxy.width = itemObject.clipDuration * timeScale
dragProxy.height = itemCoord.height
dragProxy.masterObject = itemObject
dragProxy.draggedItem = itemId
dragProxy.sourceTrack = itemTrack
dragProxy.sourceFrame = itemPos
dragProxy.isComposition = isComposition
dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
}
function endDrag() {
dragProxy.draggedItem = -1
dragProxy.x = 0
dragProxy.y = 0
dragProxy.width = 0
dragProxy.height = 0
dragProxy.verticalOffset = 0
}
function getItemAtPos(tk, posx, isComposition) {
var track = Logic.getTrackById(tk)
var container = track.children[0]
var tentativeClip = undefined
//console.log('TESTING ITMES OK TK: ', tk, ', POS: ', posx, ', CHILREN: ', container.children.length, ', COMPO: ', isComposition)
for (var i = 0 ; i < container.children.length; i++) {
if (container.children[i].children.length == 0 || container.children[i].children[0].children.length == 0) {
continue
}
tentativeClip = container.children[i].children[0].childAt(posx, 1)
if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition == isComposition)) {
//console.log('found item with id: ', tentativeClip.clipId, ' IS COMPO: ', tentativeClip.isComposition)
break
}
}
return tentativeClip
}
Keys.onDownPressed: {
root.moveSelectedTrack(1)
}
Keys.onUpPressed: {
root.moveSelectedTrack(-1)
}
property int headerWidth: timeline.headerWidth()
property int activeTool: 0
property real baseUnit: fontMetrics.font.pointSize
property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
property bool autoScrolling: timeline.autoScroll
property int duration: timeline.duration
property color audioColor: timeline.audioColor
property color videoColor: timeline.videoColor
property color lockedColor: timeline.lockedColor
property color selectionColor: timeline.selectionColor
property color groupColor: timeline.groupColor
property int clipBeingDroppedId: -1
property string clipBeingDroppedData
property int droppedPosition: -1
property int droppedTrack: -1
property int clipBeingMovedId: -1
property int spacerGroup: -1
property int spacerFrame: -1
property int spacerClickFrame: -1
property real timeScale: timeline.scaleFactor
property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? 10 / Math.sqrt(timeScale) - 0.5 : -1
property var timelineSelection: timeline.selection
property int trackHeight
property int copiedClip: -1
property int zoomOnMouse: -1
property int viewActiveTrack: timeline.activeTrack
property int wheelAccumulatedDelta: 0
readonly property int defaultDeltasPerStep: 120
//onCurrentTrackChanged: timeline.selection = []
onTimeScaleChanged: {
if (root.zoomOnMouse >= 0) {
scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
root.zoomOnMouse = -1
} else {
scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2))
}
//root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
ruler.adjustStepSize()
if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
// update dragged item pos
dragProxy.masterObject.updateDrag()
}
}
onViewActiveTrackChanged: {
var tk = Logic.getTrackById(timeline.activeTrack)
if (tk.y < scrollView.flickableItem.contentY) {
scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3)
} else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) {
scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
}
}
onActiveToolChanged: {
if (root.activeTool == 2) {
// Spacer activated
endDrag()
} else if (root.activeTool == 0) {
var tk = getMouseTrack()
if (tk < 0) {
console.log('........ MOUSE OUTSIDE TRAKS\n\n.........')
return
}
var pos = getMousePos() * timeline.scaleFactor
var sourceTrack = Logic.getTrackById(tk)
var allowComposition = tracksArea.mouseY- sourceTrack.y > sourceTrack.height / 2
var tentativeItem = undefined
if (allowComposition) {
tentativeItem = getItemAtPos(tk, pos, true)
}
if (!tentativeItem) {
tentativeItem = getItemAtPos(tk, pos, false)
}
if (tentativeItem) {
tentativeItem.updateDrag()
}
}
}
DropArea { //Drop area for compositions
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/composition'
onEntered: {
console.log("Trying to drop composition")
if (clipBeingMovedId == -1) {
console.log("No clip being moved")
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
if (track >= 0 && !controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
console.log("Trying to insert",track, frame, clipBeingDroppedData)
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
console.log("id",clipBeingDroppedId)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
drag.acceptProposedAction()
} else {
drag.accepted = false
}
}
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
if (track !=-1) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
if (controller.isAudioTrack(track)) {
// Don't allow moving composition to an audio track
track = controller.getCompositionTrackId(clipBeingDroppedId)
}
controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else if (!controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getCompositionPosition(clipBeingDroppedId)
var track = controller.getCompositionTrackId(clipBeingDroppedId)
// we simulate insertion at the final position so that stored undo has correct value
controller.requestItemDeletion(clipBeingDroppedId, false)
timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
}
clearDropData()
}
}
DropArea { //Drop area for bin/clips
/** @brief local helper function to handle the insertion of multiple dragged items */
function insertAndMaybeGroup(track, frame, droppedData) {
var binIds = droppedData.split(";")
if (binIds.length == 0) {
return -1
}
var id = -1
if (binIds.length == 1) {
id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
} else {
var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
// if the clip insertion succeeded, request the clips to be grouped
if (ids.length > 0) {
timeline.selectItems(ids)
id = ids[0]
}
}
return id
}
property int fakeFrame: -1
property int fakeTrack: -1
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/producerslist'
onEntered: {
if (clipBeingMovedId == -1) {
//var track = Logic.getTrackIdFromPos(drag.y)
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
//drag.acceptProposedAction()
clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
console.log('dropped data: ', clipBeingDroppedData)
if (controller.normalEdit()) {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
} else {
// we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
if (clipBeingDroppedId > -1) {
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
} else {
drag.accepted = false
}
}
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
drag.accepted = false
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
if (clipBeingDroppedId >= 0){
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
//controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (controller.normalEdit()) {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
} else {
// we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
}
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getClipPosition(clipBeingDroppedId)
var track = controller.getClipTrackId(clipBeingDroppedId)
if (!controller.normalEdit()) {
frame = fakeFrame
track = fakeTrack
}
/* We simulate insertion at the final position so that stored undo has correct value
* NOTE: even if dropping multiple clips, requesting the deletion of the first one is
* enough as internally it will request the group deletion
*/
controller.requestItemDeletion(clipBeingDroppedId, false)
var binIds = clipBeingDroppedData.split(";")
if (binIds.length == 1) {
if (controller.normalEdit()) {
timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
} else {
timeline.insertClipZone(clipBeingDroppedData, track, frame)
}
} else {
if (controller.normalEdit()) {
timeline.insertClips(track, frame, binIds, true, true)
} else {
// TODO
console.log('multiple clips insert/overwrite not supported yet')
}
}
fakeTrack = -1
fakeFrame = -1
}
clearDropData()
}
}
OLD.Menu {
id: menu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
editGuideMenu.visible = false
}
OLD.MenuItem {
text: i18n("Paste")
iconName: 'edit-paste'
visible: copiedClip != -1
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.pasteItem(frame, track)
}
}
OLD.MenuItem {
text: i18n("Insert Space")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.insertSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n("Remove Space On Active Track")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n("Remove Space")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame, true);
}
}
OLD.MenuItem {
id: addGuideMenu
text: i18n("Add Guide")
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
timeline.seekPosition = assetFrame
timeline.position = timeline.seekPosition
}
}
OLD.MenuItem {
id: editGuideMenu
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
AssetMenu {
title: i18n("Insert a composition...")
menuModel: transitionModel
isTransition: true
onAssetSelected: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
var id = timeline.insertComposition(track, frame, assetId, true)
if (id == -1) {
compositionFail.open()
}
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu.text = i18n("Remove Guide")
editGuideMenu.visible = true
} else {
addGuideMenu.text = i18n("Add Guide")
}
console.log("pop menu")
}
}
OLD.Menu {
id: rulermenu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
editGuideMenu2.visible = false
}
OLD.MenuItem {
id: addGuideMenu2
text: i18n("Add Guide")
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
timeline.seekPosition = assetFrame
timeline.position = timeline.seekPosition
}
}
OLD.MenuItem {
id: editGuideMenu2
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
OLD.MenuItem {
id: addProjectNote
text: i18n("Add Project Note")
onTriggered: {
timeline.triggerAction('add_project_note')
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu2.text = i18n("Remove Guide")
editGuideMenu2.visible = true
} else {
addGuideMenu2.text = i18n("Add Guide")
}
console.log("pop menu")
}
}
MessageDialog {
id: compositionFail
title: i18n("Timeline error")
icon: StandardIcon.Warning
text: i18n("Impossible to add a composition at that position. There might not be enough space")
standardButtons: StandardButton.Ok
}
OLD.Menu {
id: headerMenu
property int trackId: -1
property int thumbsFormat: 0
property bool audioTrack: false
property bool recEnabled: false
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n("Add Track")
onTriggered: {
timeline.addTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
text: i18n("Delete Track")
onTriggered: {
timeline.deleteTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: showRec
text: i18n("Show Record Controls")
onTriggered: {
controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
}
checkable: true
checked: headerMenu.recEnabled
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: configRec
text: i18n("Configure Recording")
onTriggered: {
timeline.showConfig(4,2)
}
}
OLD.Menu {
title: i18n("Track thumbnails")
visible: !headerMenu.audioTrack
OLD.ExclusiveGroup { id: thumbStyle }
OLD.MenuItem {
text: i18n("In frame")
id: inFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("In / out frames")
id: inOutFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0)
checkable: true
checked: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("All frames")
id: allFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("No thumbnails")
id: noFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3)
checkable: true
exclusiveGroup: thumbStyle
}
onAboutToShow: {
switch(headerMenu.thumbsFormat) {
case 3:
noFrame.checked = true
break
case 2:
inFrame.checked = true
break
case 1:
allFrame.checked = true
break
default:
inOutFrame.checked = true
break
}
}
}
}
Row {
Column {
id: headerContainer
z: 1
Rectangle {
id: cornerstone
property bool selected: false
// Padding between toolbar and track headers.
width: headerWidth
height: ruler.height
color: 'transparent' //selected? shotcutBlue : activePalette.window
border.color: selected? 'red' : 'transparent'
border.width: selected? 1 : 0
z: 1
}
Flickable {
// Non-slider scroll area for the track headers.
id: headerFlick
contentY: scrollView.flickableItem.contentY
width: headerWidth
height: 100
interactive: false
MouseArea {
width: trackHeaders.width
height: trackHeaders.height
acceptedButtons: Qt.NoButton
onWheel: {
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
}
}
Column {
id: trackHeaders
spacing: 0
Repeater {
id: trackHeaderRepeater
model: multitrack
TrackHead {
trackName: model.name
thumbsFormat: model.thumbsFormat
trackTag: model.trackTag
isDisabled: model.disabled
isComposite: model.composite
isLocked: model.locked
isActive: model.trackActive
isAudio: model.audio
showAudioRecord: model.audioRecord
effectNames: model.effectNames
isStackEnabled: model.isStackEnabled
width: headerWidth
current: item === timeline.activeTrack
trackId: item
height: model.trackHeight
onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
collapsed: height <= collapsedHeight
onMyTrackHeightChanged: {
collapsed = myTrackHeight <= collapsedHeight
if (!collapsed) {
controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
} else {
controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
}
// hack: change property to trigger transition adjustment
root.trackHeight = root.trackHeight === 1 ? 0 : 1
}
onClicked: {
timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
- //timeline.selectTrackHead(currentTrack)
}
}
}
}
Column {
id: trackHeadersResizer
spacing: 0
width: 5
Rectangle {
id: resizer
height: trackHeaders.height
width: 3
x: root.headerWidth - 2
color: 'red'
opacity: 0
Drag.active: headerMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: 2 * baseUnit
property double startX
property double originalX
drag.smoothed: false
onPressed: {
root.autoScrolling = false
}
onReleased: {
root.autoScrolling = timeline.autoScroll
parent.opacity = 0
}
onEntered: parent.opacity = 0.5
onExited: parent.opacity = 0
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
parent.opacity = 0.5
headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
timeline.setHeaderWidth(headerWidth)
}
}
}
}
}
}
}
MouseArea {
id: tracksArea
property real clickX
property real clickY
width: root.width - headerWidth
height: root.height
// This provides continuous scrubbing and scimming at the left/right edges.
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
onWheel: {
if (wheel.modifiers & Qt.AltModifier) {
// Alt + wheel = seek to next snap point
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else {
var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
if (timeline.seekPosition > -1) {
timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
} else {
timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
}
timeline.position = timeline.seekPosition
}
}
onPressed: {
focus = true
if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.ShiftModifier))) {
clickX = mouseX
clickY = mouseY
return
}
if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) {
console.log('1111111111111\nREAL SHIFT PRESSED\n111111111111\n')
// rubber selection
rubberSelect.x = mouse.x + tracksArea.x
rubberSelect.y = mouse.y
rubberSelect.originX = mouse.x
rubberSelect.originY = rubberSelect.y
rubberSelect.width = 0
rubberSelect.height = 0
} else if (mouse.button & Qt.LeftButton) {
if (root.activeTool === 1) {
// razor tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
}
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
if (root.activeTool === 2 && mouse.y > ruler.height) {
// spacer tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor
var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
spacerGroup = timeline.requestSpacerStartOperation(track, frame)
if (spacerGroup > -1) {
drag.axis = Drag.XAxis
Drag.active = true
Drag.proposedAction = Qt.MoveAction
spacerClickFrame = frame
spacerFrame = controller.getItemPosition(spacerGroup)
}
} else if (root.activeTool === 0 || mouse.y <= ruler.height) {
if (mouse.y > ruler.height) {
controller.requestClearSelection();
}
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
}
} else if (mouse.button & Qt.RightButton) {
menu.clickedX = mouse.x
menu.clickedY = mouse.y
if (mouse.y > ruler.height) {
timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId
menu.popup()
} else {
// ruler menu
rulermenu.popup()
}
}
}
property bool scim: false
onExited: {
scim = false
}
onPositionChanged: {
if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.ShiftModifier)))) {
var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height)
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
scrollView.flickableItem.contentY = Math.max(vertScroll, 0)
clickX = mouseX
clickY = mouseY
return
}
if (!pressed && !rubberSelect.visible && root.activeTool === 1) {
cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor)
}
}
var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor))
root.mousePosChanged(mousePos)
ruler.showZoneLabels = mouse.y < ruler.height
if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) {
// rubber selection
rubberSelect.visible = true
}
if (rubberSelect.visible) {
var newX = mouse.x
var newY = mouse.y
if (newX < rubberSelect.originX) {
rubberSelect.x = newX + tracksArea.x
rubberSelect.width = rubberSelect.originX - newX
} else {
rubberSelect.x = rubberSelect.originX + tracksArea.x
rubberSelect.width = newX - rubberSelect.originX
}
if (newY < rubberSelect.originY) {
rubberSelect.y = newY
rubberSelect.height = rubberSelect.originY - newY
} else {
rubberSelect.y = rubberSelect.originY
rubberSelect.height= newY - rubberSelect.originY
}
} else if (mouse.buttons === Qt.LeftButton) {
if (root.activeTool === 0 || mouse.y < ruler.height) {
timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
timeline.position = timeline.seekPosition
} else if (root.activeTool === 2 && spacerGroup > -1) {
// Move group
var track = controller.getItemTrackId(spacerGroup)
var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame
frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping))
continuousScrolling(mouse.x + scrollView.flickableItem.contentX)
}
scim = true
} else {
scim = false
}
}
onReleased: {
if (rubberSelect.visible) {
rubberSelect.visible = false
var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY
var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y))
var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height)
if (bottomTrack >= topTrack) {
var t = []
for (var i = topTrack; i <= bottomTrack; i++) {
t.push(tracksRepeater.itemAt(i).trackInternalId)
}
var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor
var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor
timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier);
}
rubberSelect.y = -1
} else if (mouse.modifiers & Qt.ShiftModifier) {
if (root.activeTool == 1) {
// Shift click, process seek
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
} else if (dragProxy.draggedItem > -1){
// Select item
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
console.log('ADD SELECTION: ', dragProxy.draggedItem)
controller.requestAddToSelection(dragProxy.draggedItem)
} else {
console.log('REMOVE SELECTION: ', dragProxy.draggedItem)
controller.requestRemoveFromSelection(dragProxy.draggedItem)
}
}
return
}
if (spacerGroup > -1) {
var frame = controller.getItemPosition(spacerGroup)
timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame);
spacerClickFrame = -1
spacerFrame = -1
spacerGroup = -1
}
scim = false
}
Column {
Flickable {
// Non-slider scroll area for the Ruler.
id: rulercontainer
width: root.width - headerWidth
height: fontMetrics.font.pixelSize * 2
contentX: scrollView.flickableItem.contentX
contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale)
interactive: false
clip: true
Ruler {
id: ruler
width: rulercontainer.contentWidth
height: parent.height
Rectangle {
id: seekCursor
visible: timeline.seekPosition > -1
color: activePalette.highlight
width: 4
height: ruler.height
opacity: 0.5
x: timeline.seekPosition * timeline.scaleFactor
}
TimelinePlayhead {
id: playhead
visible: timeline.position > -1
height: baseUnit
width: baseUnit * 1.5
fillColor: activePalette.windowText
anchors.bottom: parent.bottom
x: timeline.position * timeline.scaleFactor - (width / 2)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
}
}
OLD.ScrollView {
id: scrollView
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
// Click and drag should seek, not scroll the timeline view
flickableItem.interactive: false
clip: true
Rectangle {
id: tracksContainerArea
width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale)
height: trackHeaders.height
//Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height)
color: root.color
Rectangle {
// Drag proxy, responsible for clip / composition move
id: dragProxy
x: 0
y: 0
width: 0
height: 0
property int draggedItem: -1
property int sourceTrack
property int sourceFrame
property bool isComposition
property int verticalOffset
property var masterObject
color: 'transparent'
//opacity: 0.8
MouseArea {
id: dragProxyArea
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
drag.minimumX: 0
property int dragFrame
property bool moveMirrorTracks: true
cursorShape: root.activeTool == 0 ? dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor : tracksArea.cursorShape
enabled: root.activeTool == 0
onPressed: {
console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++')
if (mouse.modifiers & Qt.ControlModifier || mouse.modifiers & Qt.ShiftModifier) {
mouse.accepted = false
console.log('+++++++++++++++++++ Shift abort+++++++++++++')
return
}
if (!timeline.exists(dragProxy.draggedItem)) {
endDrag()
mouse.accepted = false
return
}
dragFrame = -1
moveMirrorTracks = !(mouse.modifiers & Qt.MetaModifier)
timeline.activeTrack = dragProxy.sourceTrack
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true)
}
timeline.showAsset(dragProxy.draggedItem)
root.autoScrolling = false
clipBeingMovedId = dragProxy.draggedItem
if (dragProxy.draggedItem > -1) {
var tk = controller.getItemTrackId(dragProxy.draggedItem)
var x = controller.getItemPosition(dragProxy.draggedItem)
var posx = Math.round((parent.x)/ root.timeScale)
var clickAccepted = true
var currentMouseTrack = Logic.getTrackIdFromPos(parent.y)
if (controller.normalEdit() && (tk != currentMouseTrack || x != posx)) {
console.log('INCORRECT DRAG, Trying to recover item: ', parent.y,' XPOS: ',x,'=',posx,'on track: ',tk ,'\n!!!!!!!!!!')
// Try to find correct item
var tentativeClip = getItemAtPos(currentMouseTrack, mouseX + parent.x, dragProxy.isComposition)
if (tentativeClip && tentativeClip.clipId) {
console.log('FOUND MISSING ITEM: ', tentativeClip.clipId)
clickAccepted = true
dragProxy.draggedItem = tentativeClip.clipId
dragProxy.x = tentativeClip.x
dragProxy.y = tentativeClip.y
dragProxy.width = tentativeClip.width
dragProxy.height = tentativeClip.height
dragProxy.masterObject = tentativeClip
dragProxy.sourceTrack = tk
dragProxy.sourceFrame = tentativeClip.modelStart
dragProxy.isComposition = tentativeClip.isComposition
} else {
console.log('COULD NOT FIND ITEM ')
clickAccepted = false
mouse.accepted = false
dragProxy.draggedItem = -1
dragProxy.masterObject = undefined
dragProxy.sourceFrame = -1
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
if (clickAccepted && dragProxy.draggedItem != -1) {
focus = true;
dragProxy.masterObject.originalX = dragProxy.masterObject.x
dragProxy.masterObject.originalTrackId = dragProxy.masterObject.trackId
dragProxy.masterObject.forceActiveFocus();
}
} else {
mouse.accepted = false
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
onPositionChanged: {
// we have to check item validity in the controller, because they could have been deleted since the beginning of the drag
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
return
}
if (dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
continuousScrolling(mouse.x + parent.x)
var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
var posx = Math.round((parent.x)/ root.timeScale)
var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height)
var tId = Logic.getTrackIdFromPos(posy)
if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) {
if (posx == dragFrame && controller.normalEdit()) {
return
}
}
if (dragProxy.isComposition) {
dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
} else {
if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) {
var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y);
dragProxy.masterObject.parent = dragContainer
pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y)
dragProxy.masterObject.x = pos.x
dragProxy.masterObject.y = pos.y
//console.log('bringing item to front')
}
dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping), moveMirrorTracks)
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
}
var delta = dragFrame - dragProxy.sourceFrame
if (delta != 0) {
var s = timeline.simplifiedTC(Math.abs(delta))
s = ((delta < 0)? '-' : '+') + s + i18n("\nPosition:%1", timeline.simplifiedTC(dragFrame))
bubbleHelp.show(parent.x + mouseX, Math.max(ruler.height, Logic.getTrackYFromId(timeline.activeTrack)), s)
} else bubbleHelp.hide()
}
}
onReleased: {
clipBeingMovedId = -1
root.autoScrolling = timeline.autoScroll
if (dragProxy.draggedItem > -1 && dragFrame > -1 && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
var tId = controller.getItemTrackId(dragProxy.draggedItem)
if (dragProxy.isComposition) {
controller.requestCompositionMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestCompositionMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
if (controller.normalEdit()) {
controller.requestClipMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, moveMirrorTracks, true, false, false)
controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , moveMirrorTracks, true, true, true)
} else {
// Fake move, only process final move
timeline.endFakeMove(dragProxy.draggedItem, dragFrame, true, true, true)
}
}
if (dragProxy.masterObject && dragProxy.masterObject.isGrabbed) {
dragProxy.masterObject.grabItem()
}
dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor
dragProxy.sourceFrame = dragFrame
bubbleHelp.hide()
}
}
onDoubleClicked: {
if (dragProxy.masterObject.keyframeModel) {
var newVal = (dragProxy.height - mouseY) / dragProxy.height
var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint
timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal)
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
Column {
// These make the striped background for the tracks.
// It is important that these are not part of the track visual hierarchy;
// otherwise, the clips will be obscured by the Track's background.
Repeater {
model: multitrack
id: trackBaseRepeater
delegate: Rectangle {
width: tracksContainerArea.width
border.width: 1
border.color: root.frameColor
height: model.trackHeight
color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red'
}
}
}
Column {
id: tracksContainer
Repeater { id: tracksRepeater; model: trackDelegateModel }
Item {
id: dragContainer
z: 100
}
Repeater { id: guidesRepeater; model: guidesDelegateModel }
}
Rectangle {
id: cursor
visible: timeline.position > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: timeline.position * timeline.scaleFactor
}
}
}
}
/*CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null
opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0
}
CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null
opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0
anchors.right: parent.right
mirrorGradient: true
}*/
Rectangle {
id: cutLine
visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height
color: 'red'
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: root.height - scrollView.__horizontalScrollBar.height - ruler.height
x: 0
//x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX
y: ruler.height
}
}
}
Rectangle {
id: bubbleHelp
property alias text: bubbleHelpLabel.text
color: root.color //application.toolTipBaseColor
width: bubbleHelpLabel.width + 8
height: bubbleHelpLabel.height + 8
radius: 4
states: [
State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} },
State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} }
]
state: 'invisible'
transitions: [
Transition {
from: 'invisible'
to: 'visible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
},
Transition {
from: 'visible'
to: 'invisible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
}
]
Label {
id: bubbleHelpLabel
color: activePalette.text //application.toolTipTextColor
anchors.centerIn: parent
font.pixelSize: root.baseUnit
}
function show(x, y, text) {
bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width
bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height
bubbleHelp.text = text
if (bubbleHelp.state !== 'visible')
bubbleHelp.state = 'visible'
}
function hide() {
bubbleHelp.state = 'invisible'
bubbleHelp.opacity = 0
}
}
Rectangle {
id: rubberSelect
property int originX
property int originY
y: -1
color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4)
border.color: activePalette.highlight
border.width: 1
visible: false
}
/*DropShadow {
source: bubbleHelp
anchors.fill: bubbleHelp
opacity: bubbleHelp.opacity
horizontalOffset: 3
verticalOffset: 3
radius: 8
color: '#80000000'
transparentBorder: true
fast: true
}*/
DelegateModel {
id: trackDelegateModel
model: multitrack
delegate: Track {
trackModel: multitrack
rootIndex: trackDelegateModel.modelIndex(index)
timeScale: timeline.scaleFactor
width: tracksContainerArea.width
height: trackHeight
isAudio: audio
trackThumbsFormat: thumbsFormat
isCurrentTrack: item === timeline.activeTrack
trackInternalId: item
z: tracksRepeater.count - index
}
}
DelegateModel {
id: guidesDelegateModel
model: guidesModel
Item {
id: guideRoot
z: 20
Rectangle {
id: guideBase
width: 1
height: tracksContainer.height
x: model.frame * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: guideBase.x
y: mlabel.y
radius: 2
width: mlabel.width + 4
height: mlabel.height
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int startX
drag.axis: Drag.XAxis
drag.target: guideRoot
onPressed: {
drag.target = guideRoot
startX = guideRoot.x
}
onReleased: {
if (startX != guideRoot.x) {
timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor)
}
drag.target = undefined
}
onPositionChanged: {
if (pressed) {
var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, root.snapping)
guideRoot.x = (frame - model.frame) * timeline.scaleFactor
}
}
drag.smoothed: false
onDoubleClicked: {
timeline.editGuide(model.frame)
drag.target = undefined
}
onClicked: timeline.position = guideBase.x / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers
text: model.comment
font.pixelSize: root.baseUnit
x: guideBase.x + 2
y: scrollView.flickableItem.contentY
color: 'white'
}
}
}
Connections {
target: timeline
onPositionChanged: if (autoScrolling) Logic.scrollIfNeeded()
onFrameFormatChanged: ruler.adjustFormat()
onSelectionChanged: {
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
}
}
}
// This provides continuous scrolling at the left/right edges.
Timer {
id: scrollTimer
interval: 25
repeat: true
triggeredOnStart: true
property var item
property bool backwards
onTriggered: {
var delta = backwards? -10 : 10
if (item) item.x += delta
scrollView.flickableItem.contentX += delta
if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1)
stop()
}
}
}