diff --git a/src/abstractmodel/abstracttreemodel.cpp b/src/abstractmodel/abstracttreemodel.cpp
index df060e1a1..c3a07b777 100644
--- a/src/abstractmodel/abstracttreemodel.cpp
+++ b/src/abstractmodel/abstracttreemodel.cpp
@@ -1,349 +1,352 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "abstracttreemodel.hpp"
#include "treeitem.hpp"
#include
#include
#include
#include
int AbstractTreeModel::currentTreeId = 0;
AbstractTreeModel::AbstractTreeModel(QObject *parent)
: QAbstractItemModel(parent)
{
}
std::shared_ptr AbstractTreeModel::construct(QObject *parent)
{
std::shared_ptr self(new AbstractTreeModel(parent));
self->rootItem = TreeItem::construct(QList(), self, true);
return self;
}
AbstractTreeModel::~AbstractTreeModel()
{
m_allItems.clear();
rootItem.reset();
}
int AbstractTreeModel::columnCount(const QModelIndex &parent) const
{
if (!parent.isValid()) return rootItem->columnCount();
const auto id = (int)parent.internalId();
auto item = getItemById(id);
return item->columnCount();
}
QVariant AbstractTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
if (role != Qt::DisplayRole) {
return QVariant();
}
auto item = getItemById((int)index.internalId());
return item->dataColumn(index.column());
}
Qt::ItemFlags AbstractTreeModel::flags(const QModelIndex &index) const
{
const auto flags = QAbstractItemModel::flags(index);
if (index.isValid()) {
auto item = getItemById((int)index.internalId());
if (item->depth() == 1) {
return flags & ~Qt::ItemIsSelectable;
}
}
return flags;
}
QVariant AbstractTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return rootItem->dataColumn(section);
return QVariant();
}
QModelIndex AbstractTreeModel::index(int row, int column, const QModelIndex &parent) const
{
std::shared_ptr parentItem;
if (!parent.isValid())
parentItem = rootItem;
else
parentItem = getItemById((int)parent.internalId());
if (row >= parentItem->childCount()) return QModelIndex();
std::shared_ptr childItem = parentItem->child(row);
if (childItem) return createIndex(row, column, quintptr(childItem->getId()));
return {};
}
QModelIndex AbstractTreeModel::parent(const QModelIndex &index) const
{
if (!index.isValid()) return {};
std::shared_ptr childItem = getItemById((int)index.internalId());
std::shared_ptr parentItem = childItem->parentItem().lock();
Q_ASSERT(parentItem);
if (parentItem == rootItem) return QModelIndex();
return createIndex(parentItem->row(), 0, quintptr(parentItem->getId()));
}
int AbstractTreeModel::rowCount(const QModelIndex &parent) const
{
if (parent.column() > 0) return 0;
std::shared_ptr parentItem;
if (!parent.isValid())
parentItem = rootItem;
else
parentItem = getItemById((int)parent.internalId());
return parentItem->childCount();
}
QModelIndex AbstractTreeModel::getIndexFromItem(const std::shared_ptr &item) const
{
if (item == rootItem) {
- return {};
+ return QModelIndex();
+ }
+ if (auto ptr = item->parentItem().lock()) {
+ auto parentIndex = getIndexFromItem(ptr);
+ return index(item->row(), 0, parentIndex);
}
- auto parentIndex = getIndexFromItem(item->parentItem().lock());
- return index(item->row(), 0, parentIndex);
+ return QModelIndex();
}
QModelIndex AbstractTreeModel::getIndexFromId(int id) const
{
if (id == rootItem->getId()) {
return QModelIndex();
}
Q_ASSERT(m_allItems.count(id) > 0);
if (auto ptr = m_allItems.at(id).lock()) return getIndexFromItem(ptr);
Q_ASSERT(false);
return {};
}
void AbstractTreeModel::notifyRowAboutToAppend(const std::shared_ptr &item)
{
auto index = getIndexFromItem(item);
beginInsertRows(index, item->childCount(), item->childCount());
}
void AbstractTreeModel::notifyRowAppended(const std::shared_ptr &row)
{
Q_UNUSED(row);
endInsertRows();
}
void AbstractTreeModel::notifyRowAboutToDelete(std::shared_ptr item, int row)
{
auto index = getIndexFromItem(item);
beginRemoveRows(index, row, row);
}
void AbstractTreeModel::notifyRowDeleted()
{
endRemoveRows();
}
// static
int AbstractTreeModel::getNextId()
{
return currentTreeId++;
}
void AbstractTreeModel::registerItem(const std::shared_ptr &item)
{
int id = item->getId();
Q_ASSERT(m_allItems.count(id) == 0);
m_allItems[id] = item;
}
void AbstractTreeModel::deregisterItem(int id, TreeItem *item)
{
Q_UNUSED(item);
Q_ASSERT(m_allItems.count(id) > 0);
m_allItems.erase(id);
}
std::shared_ptr AbstractTreeModel::getItemById(int id) const
{
if (id == rootItem->getId()) {
return rootItem;
}
Q_ASSERT(m_allItems.count(id) > 0);
return m_allItems.at(id).lock();
}
std::shared_ptr AbstractTreeModel::getRoot() const
{
return rootItem;
}
bool AbstractTreeModel::checkConsistency()
{
// first check that the root is all good
if (!rootItem || !rootItem->m_isRoot || !rootItem->isInModel() || m_allItems.count(rootItem->getId()) == 0) {
qDebug() << !rootItem->m_isRoot << !rootItem->isInModel() << (m_allItems.count(rootItem->getId()) == 0);
qDebug() << "ERROR: Model is not valid because root is not properly constructed";
return false;
}
// Then we traverse the tree from the root, checking the infos on the way
std::unordered_set seenIDs;
std::queue>> queue; // store (id, (depth, parentId))
queue.push({rootItem->getId(), {0, rootItem->getId()}});
while (!queue.empty()) {
auto current = queue.front();
int currentId = current.first, currentDepth = current.second.first;
int parentId = current.second.second;
queue.pop();
if (seenIDs.count(currentId) != 0) {
qDebug() << "ERROR: Invalid tree: Id found twice."
<< "It either a cycle or a clash in id attribution";
return false;
}
if (m_allItems.count(currentId) == 0) {
qDebug() << "ERROR: Invalid tree: Id not found. Item is not registered";
return false;
}
auto currentItem = m_allItems[currentId].lock();
if (currentItem->depth() != currentDepth) {
qDebug() << "ERROR: Invalid tree: invalid depth info found";
return false;
}
if (!currentItem->isInModel()) {
qDebug() << "ERROR: Invalid tree: item thinks it is not in a model";
return false;
}
if (currentId != rootItem->getId()) {
if ((currentDepth == 0 || currentItem->m_isRoot)) {
qDebug() << "ERROR: Invalid tree: duplicate root";
return false;
}
if (auto ptr = currentItem->parentItem().lock()) {
if (ptr->getId() != parentId || ptr->child(currentItem->row())->getId() != currentItem->getId()) {
qDebug() << "ERROR: Invalid tree: invalid parent link";
return false;
}
} else {
qDebug() << "ERROR: Invalid tree: invalid parent";
return false;
}
}
// propagate to children
int i = 0;
for (const auto &child : currentItem->m_childItems) {
if (currentItem->child(i) != child) {
qDebug() << "ERROR: Invalid tree: invalid child ordering";
return false;
}
queue.push({child->getId(), {currentDepth + 1, currentId}});
i++;
}
}
return true;
}
Fun AbstractTreeModel::addItem_lambda(const std::shared_ptr &new_item, int parentId)
{
return [this, new_item, parentId]() {
/* Insertion is simply setting the parent of the item.*/
std::shared_ptr parent;
if (parentId != -1) {
parent = getItemById(parentId);
if (!parent) {
Q_ASSERT(parent);
return false;
}
}
return new_item->changeParent(parent);
};
}
Fun AbstractTreeModel::removeItem_lambda(int id)
{
return [this, id]() {
/* Deletion simply deregister clip and remove it from parent.
The actual object is not actually deleted, because a shared_pointer to it
is captured by the reverse operation.
Actual deletions occurs when the undo object is destroyed.
*/
auto item = m_allItems[id].lock();
Q_ASSERT(item);
if (!item) {
return false;
}
auto parent = item->parentItem().lock();
parent->removeChild(item);
return true;
};
}
Fun AbstractTreeModel::moveItem_lambda(int id, int destRow, bool force)
{
Fun lambda = []() { return true; };
std::vector> oldStack;
auto item = getItemById(id);
if (!force && item->row() == destRow) {
// nothing to do
return lambda;
}
if (auto parent = item->parentItem().lock()) {
if (destRow > parent->childCount() || destRow < 0) {
return []() { return false; };
}
int parentId = parent->getId();
// remove the element to move
oldStack.push_back(item);
Fun oper = removeItem_lambda(id);
PUSH_LAMBDA(oper, lambda);
// remove the tail of the stack
for (int i = destRow; i < parent->childCount(); ++i) {
auto current = parent->child(i);
if (current->getId() != id) {
oldStack.push_back(current);
oper = removeItem_lambda(current->getId());
PUSH_LAMBDA(oper, lambda);
}
}
// insert back in order
for (const auto &elem : oldStack) {
oper = addItem_lambda(elem, parentId);
PUSH_LAMBDA(oper, lambda);
}
return lambda;
}
return []() { return false; };
}
diff --git a/src/effects/effectstack/view/effectstackview.cpp b/src/effects/effectstack/view/effectstackview.cpp
index dc33830e9..c7a5da31f 100644
--- a/src/effects/effectstack/view/effectstackview.cpp
+++ b/src/effects/effectstack/view/effectstackview.cpp
@@ -1,408 +1,410 @@
/***************************************************************************
* Copyright (C) 2017 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "effectstackview.hpp"
#include "assets/assetlist/view/qmltypes/asseticonprovider.hpp"
#include "assets/assetpanel.hpp"
#include "assets/view/assetparameterview.hpp"
#include "builtstack.hpp"
#include "collapsibleeffectview.hpp"
#include "core.h"
#include "effects/effectstack/model/effectitemmodel.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
#include "monitor/monitor.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
WidgetDelegate::WidgetDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
QSize WidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QSize s = QStyledItemDelegate::sizeHint(option, index);
if (m_height.contains(index)) {
s.setHeight(m_height.value(index));
}
return s;
}
void WidgetDelegate::setHeight(const QModelIndex &index, int height)
{
m_height[index] = height;
emit sizeHintChanged(index);
}
int WidgetDelegate::height(const QModelIndex &index) const
{
return m_height.value(index);
}
void WidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem opt(option);
initStyleOption(&opt, index);
QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
}
EffectStackView::EffectStackView(AssetPanel *parent)
: QWidget(parent)
, m_model(nullptr)
, m_thumbnailer(new AssetIconProvider(true))
{
m_lay = new QVBoxLayout(this);
m_lay->setContentsMargins(0, 0, 0, 0);
m_lay->setSpacing(0);
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
setAcceptDrops(true);
/*m_builtStack = new BuiltStack(parent);
m_builtStack->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
m_lay->addWidget(m_builtStack);
m_builtStack->setVisible(KdenliveSettings::showbuiltstack());*/
m_effectsTree = new QTreeView(this);
m_effectsTree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
m_effectsTree->setHeaderHidden(true);
m_effectsTree->setRootIsDecorated(false);
QString style = QStringLiteral("QTreeView {border: none;}");
// m_effectsTree->viewport()->setAutoFillBackground(false);
m_effectsTree->setStyleSheet(style);
m_effectsTree->setVisible(!KdenliveSettings::showbuiltstack());
m_lay->addWidget(m_effectsTree);
m_lay->addStretch(10);
m_scrollTimer.setSingleShot(true);
m_scrollTimer.setInterval(250);
connect(&m_scrollTimer, &QTimer::timeout, this, &EffectStackView::checkScrollBar);
}
EffectStackView::~EffectStackView()
{
delete m_thumbnailer;
}
void EffectStackView::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
} else {
event->setDropAction(Qt::CopyAction);
}
event->setAccepted(true);
} else {
event->setAccepted(false);
}
}
void EffectStackView::dropEvent(QDropEvent *event)
{
event->accept();
QString effectId = event->mimeData()->data(QStringLiteral("kdenlive/effect"));
int row = m_model->rowCount();
for (int i = 0; i < m_model->rowCount(); i++) {
auto item = m_model->getEffectStackRow(i);
if (item->childCount() > 0) {
// TODO: group
continue;
}
std::shared_ptr eff = std::static_pointer_cast(item);
QModelIndex ix = m_model->getIndexFromItem(eff);
QWidget *w = m_effectsTree->indexWidget(ix);
if (w && w->geometry().contains(event->pos())) {
qDebug() << "// DROPPED ON EFF: " << eff->getAssetId();
row = i;
break;
}
}
if (event->source() == this) {
QString sourceData = event->mimeData()->data(QStringLiteral("kdenlive/effectsource"));
int oldRow = sourceData.section(QLatin1Char('-'), 2, 2).toInt();
qDebug() << "// MOVING EFFECT FROM : " << oldRow << " TO " << row;
if (row == oldRow || (row == m_model->rowCount() && oldRow == row - 1)) {
return;
}
m_model->moveEffect(row, m_model->getEffectStackRow(oldRow));
} else {
bool added = false;
if (row < m_model->rowCount()) {
if (m_model->appendEffect(effectId)) {
added = true;
m_model->moveEffect(row, m_model->getEffectStackRow(m_model->rowCount() - 1));
}
} else {
if (m_model->appendEffect(effectId)) {
added = true;
std::shared_ptr item = m_model->getEffectStackRow(m_model->rowCount() - 1);
if (item) {
slotActivateEffect(std::static_pointer_cast(item));
}
}
}
if (!added) {
pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage);
} else {
m_scrollTimer.start();
}
}
}
void EffectStackView::setModel(std::shared_ptr model, const QSize frameSize)
{
qDebug() << "MUTEX LOCK!!!!!!!!!!!! setmodel";
m_mutex.lock();
unsetModel(false);
m_model = std::move(model);
m_sourceFrameSize = frameSize;
m_effectsTree->setModel(m_model.get());
m_effectsTree->setItemDelegateForColumn(0, new WidgetDelegate(this));
m_effectsTree->setColumnHidden(1, true);
m_effectsTree->setAcceptDrops(true);
m_effectsTree->setDragDropMode(QAbstractItemView::DragDrop);
m_effectsTree->setDragEnabled(true);
m_effectsTree->setUniformRowHeights(false);
m_mutex.unlock();
qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! setmodel";
loadEffects();
m_scrollTimer.start();
connect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh);
connect(m_model.get(), &EffectStackModel::enabledStateChanged, this, &EffectStackView::updateEnabledState);
connect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect);
// m_builtStack->setModel(model, stackOwner());
}
void EffectStackView::loadEffects()
{
qDebug() << "MUTEX LOCK!!!!!!!!!!!! loadEffects: ";
//QMutexLocker lock(&m_mutex);
int max = m_model->rowCount();
if (max == 0) {
// blank stack
ObjectId item = m_model->getOwnerId();
pCore->getMonitor(item.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)->slotShowEffectScene(MonitorSceneDefault);
return;
}
int active = qBound(0, m_model->getActiveEffect(), max - 1);
for (int i = 0; i < max; i++) {
std::shared_ptr item = m_model->getEffectStackRow(i);
QSize size;
if (item->childCount() > 0) {
// group, create sub stack
continue;
}
std::shared_ptr effectModel = std::static_pointer_cast(item);
CollapsibleEffectView *view = nullptr;
// We need to rebuild the effect view
QImage effectIcon = m_thumbnailer->requestImage(effectModel->getAssetId(), &size, QSize(QStyle::PM_SmallIconSize, QStyle::PM_SmallIconSize));
view = new CollapsibleEffectView(effectModel, m_sourceFrameSize, effectIcon, this);
connect(view, &CollapsibleEffectView::deleteEffect, m_model.get(), &EffectStackModel::removeEffect);
connect(view, &CollapsibleEffectView::moveEffect, m_model.get(), &EffectStackModel::moveEffect);
connect(view, &CollapsibleEffectView::reloadEffect, this, &EffectStackView::reloadEffect);
connect(view, &CollapsibleEffectView::switchHeight, this, &EffectStackView::slotAdjustDelegate, Qt::DirectConnection);
connect(view, &CollapsibleEffectView::startDrag, this, &EffectStackView::slotStartDrag);
connect(view, &CollapsibleEffectView::createGroup, m_model.get(), &EffectStackModel::slotCreateGroup);
connect(view, &CollapsibleEffectView::activateEffect, this, &EffectStackView::slotActivateEffect);
connect(this, &EffectStackView::blockWheenEvent, view, &CollapsibleEffectView::blockWheenEvent);
connect(view, &CollapsibleEffectView::seekToPos, [this](int pos) {
// at this point, the effects returns a pos relative to the clip. We need to convert it to a global time
int clipIn = pCore->getItemPosition(m_model->getOwnerId());
emit seekToPos(pos + clipIn);
});
connect(this, &EffectStackView::doActivateEffect, view, &CollapsibleEffectView::slotActivateEffect);
QModelIndex ix = m_model->getIndexFromItem(effectModel);
m_effectsTree->setIndexWidget(ix, view);
auto *del = static_cast(m_effectsTree->itemDelegate(ix));
del->setHeight(ix, view->height());
view->buttonUp->setEnabled(i > 0);
view->buttonDown->setEnabled(i < max - 1);
if (i == active) {
m_model->setActiveEffect(i);
emit doActivateEffect(ix);
}
}
updateTreeHeight();
qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! loadEffects";
}
void EffectStackView::updateTreeHeight()
{
// For some reason, the treeview height does not update correctly, so enforce it
m_mutex.lock();
int totalHeight = 0;
for (int j = 0; j < m_model->rowCount(); j++) {
std::shared_ptr item2 = m_model->getEffectStackRow(j);
std::shared_ptr eff = std::static_pointer_cast(item2);
QModelIndex idx = m_model->getIndexFromItem(eff);
auto w = m_effectsTree->indexWidget(idx);
if (w) {
totalHeight += w->height();
}
}
m_effectsTree->setFixedHeight(totalHeight);
m_mutex.unlock();
m_scrollTimer.start();
}
void EffectStackView::slotActivateEffect(const std::shared_ptr &effectModel)
{
qDebug() << "MUTEX LOCK!!!!!!!!!!!! slotactivateeffect: " << effectModel->row();
QMutexLocker lock(&m_mutex);
m_model->setActiveEffect(effectModel->row());
QModelIndex activeIx = m_model->getIndexFromItem(effectModel);
emit doActivateEffect(activeIx);
qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! slotactivateeffect";
}
void EffectStackView::slotStartDrag(const QPixmap &pix, const std::shared_ptr &effectModel)
{
auto *drag = new QDrag(this);
drag->setPixmap(pix);
auto *mime = new QMimeData;
mime->setData(QStringLiteral("kdenlive/effect"), effectModel->getAssetId().toUtf8());
// TODO this will break if source effect is not on the stack of a timeline clip
ObjectId source = effectModel->getOwnerId();
QByteArray effectSource;
effectSource += QString::number((int)source.first).toUtf8();
effectSource += '-';
effectSource += QString::number((int)source.second).toUtf8();
effectSource += '-';
effectSource += QString::number(effectModel->row()).toUtf8();
mime->setData(QStringLiteral("kdenlive/effectsource"), effectSource);
// mime->setData(QStringLiteral("kdenlive/effectrow"), QString::number(effectModel->row()).toUtf8());
// Assign ownership of the QMimeData object to the QDrag object.
drag->setMimeData(mime);
// Start the drag and drop operation
drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
}
void EffectStackView::slotAdjustDelegate(const std::shared_ptr &effectModel, int newHeight)
{
if (!m_model) {
return;
}
QModelIndex ix = m_model->getIndexFromItem(effectModel);
- auto *del = static_cast(m_effectsTree->itemDelegate(ix));
- if (del) {
- del->setHeight(ix, newHeight);
- QMetaObject::invokeMethod(this, "updateTreeHeight", Qt::QueuedConnection);
+ if (ix.isValid()) {
+ auto *del = static_cast(m_effectsTree->itemDelegate(ix));
+ if (del) {
+ del->setHeight(ix, newHeight);
+ QMetaObject::invokeMethod(this, "updateTreeHeight", Qt::QueuedConnection);
+ }
}
}
void EffectStackView::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
m_scrollTimer.start();
}
void EffectStackView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles)
{
Q_UNUSED(roles)
if (!topLeft.isValid() || !bottomRight.isValid()) {
loadEffects();
return;
}
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
for (int j = topLeft.column(); j <= bottomRight.column(); ++j) {
CollapsibleEffectView *w = static_cast(m_effectsTree->indexWidget(m_model->index(i, j, topLeft.parent())));
if (w) {
w->refresh();
}
}
}
}
void EffectStackView::unsetModel(bool reset)
{
// Release ownership of smart pointer
Kdenlive::MonitorId id = Kdenlive::NoMonitor;
if (m_model) {
ObjectId item = m_model->getOwnerId();
id = item.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor;
disconnect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh);
disconnect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect);
}
if (reset) {
QMutexLocker lock(&m_mutex);
m_model.reset();
m_effectsTree->setModel(nullptr);
}
if (id != Kdenlive::NoMonitor) {
pCore->getMonitor(id)->slotShowEffectScene(MonitorSceneDefault);
}
}
ObjectId EffectStackView::stackOwner() const
{
if (m_model) {
return m_model->getOwnerId();
}
return ObjectId(ObjectType::NoItem, -1);
}
bool EffectStackView::addEffect(const QString &effectId)
{
if (m_model) {
return m_model->appendEffect(effectId, true);
}
return false;
}
bool EffectStackView::isEmpty() const
{
return m_model == nullptr ? true : m_model->rowCount() == 0;
}
void EffectStackView::enableStack(bool enable)
{
if (m_model) {
m_model->setEffectStackEnabled(enable);
}
}
bool EffectStackView::isStackEnabled() const
{
if (m_model) {
return m_model->isStackEnabled();
}
return false;
}
/*
void EffectStackView::switchBuiltStack(bool show)
{
m_builtStack->setVisible(show);
m_effectsTree->setVisible(!show);
KdenliveSettings::setShowbuiltstack(show);
}
*/
diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml
index 120959591..fd9bf4c53 100644
--- a/src/timeline2/view/qml/Clip.qml
+++ b/src/timeline2/view/qml/Clip.qml
@@ -1,972 +1,961 @@
/*
* 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.11
import QtQuick.Controls 2.4
import Kdenlive.Controls 1.0
import QtQml.Models 2.11
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
import com.enums 1.0
Rectangle {
id: clipRoot
property real timeScale: 1.0
property string clipName: ''
property string clipResource: ''
property string mltService: ''
property string effectNames
property int modelStart
property real scrollX: 0
property int inPoint: 0
property int outPoint: 0
property int clipDuration: 0
property int maxDuration: 0
property bool isAudio: false
property int audioChannels
property bool showKeyframes: false
property bool isGrabbed: false
property bool grouped: false
property var markers
property var keyframeModel
property int clipStatus: 0
property int itemType: 0
property int fadeIn: 0
property int fadeOut: 0
property int binId: 0
property int positionOffset: 0
property var parentTrack
property int trackIndex //Index in track repeater
property int clipId //Id of the clip in the model
property int trackId: -1 // Id of the parent track in the model
property int fakeTid: -1
property int fakePosition: 0
property int originalTrackId: -1
property int originalX: x
property int originalDuration: clipDuration
property int lastValidDuration: clipDuration
property int draggedX: x
property bool selected: false
property bool isLocked: parentTrack && parentTrack.isLocked == true
property bool hasAudio
property bool canBeAudio
property bool canBeVideo
property double speed: 1.0
property color borderColor: 'black'
property bool forceReloadThumb
property bool isComposition: false
property bool hideClipViews
property var groupTrimData
property int scrollStart: scrollView.flickableItem.contentX - clipRoot.modelStart * timeline.scaleFactor
width : clipDuration * timeScale;
opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0
signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim, bool controlTrim)
signal trimmedIn(var clip, bool shiftTrim, bool controlTrim)
signal initGroupTrim(var clip)
signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim, bool controlTrim)
signal trimmedOut(var clip, bool shiftTrim, bool controlTrim)
onScrollStartChanged: {
clipRoot.hideClipViews = scrollStart > width || scrollStart + scrollView.viewport.width < 0
}
onIsGrabbedChanged: {
if (clipRoot.isGrabbed) {
grabItem()
} else {
mouseArea.focus = false
}
}
function grabItem() {
clipRoot.forceActiveFocus()
mouseArea.focus = true
}
function clearAndMove(offset) {
controller.requestClearSelection()
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - offset, true, true, true)
controller.requestAddToSelection(clipRoot.clipId)
}
onClipResourceChanged: {
if (itemType == ProducerType.Color) {
color: Qt.darker(getColor(), 1.5)
}
}
ToolTip {
visible: mouseArea.containsMouse && !dragProxyArea.pressed
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
font.pointSize: root.fontUnit
text: label.text + ' (' + timeline.simplifiedTC(clipRoot.inPoint) + '-' + timeline.simplifiedTC(clipRoot.outPoint) + ')'
}
}
onKeyframeModelChanged: {
if (effectRow.keyframecanvas) {
console.log('keyframe model changed............')
effectRow.keyframecanvas.requestPaint()
}
}
onClipDurationChanged: {
width = clipDuration * timeScale;
if (parentTrack && parentTrack.isAudio && thumbsLoader.item) {
// Duration changed, we may need a different number of repeaters
thumbsLoader.item.reload()
}
}
onModelStartChanged: {
x = modelStart * timeScale;
}
onFakePositionChanged: {
x = fakePosition * timeScale;
}
onFakeTidChanged: {
if (clipRoot.fakeTid > -1 && parentTrack) {
if (clipRoot.parent != dragContainer) {
var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y);
clipRoot.parent = dragContainer
pos = clipRoot.mapFromGlobal(pos.x, pos.y)
clipRoot.x = pos.x
clipRoot.y = pos.y
}
clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y
}
}
onForceReloadThumbChanged: {
// TODO: find a way to force reload of clip thumbs
if (thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
onTimeScaleChanged: {
x = modelStart * timeScale;
width = clipDuration * timeScale;
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
}
onScrollXChanged: {
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
}
border.color: selected ? root.selectionColor : grouped ? root.groupColor : borderColor
border.width: isGrabbed ? 8 : 1.5
function updateDrag() {
var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
function getColor() {
if (clipStatus == ClipState.Disabled) {
return 'grey'
}
if (itemType == ProducerType.Color) {
var color = clipResource.substring(clipResource.length - 9)
if (color[0] == '#') {
return color
}
return '#' + color.substring(color.length - 8, color.length - 2)
}
return isAudio? root.audioColor : root.videoColor
}
/* function reparent(track) {
console.log('TrackId: ',trackId)
parent = track
height = track.height
parentTrack = track
trackId = parentTrack.trackId
console.log('Reparenting clip to Track: ', trackId)
//generateWaveform()
}
*/
property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '')
property bool isImage: itemType == ProducerType.Image
property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + documentId + '/' + (isImage ? '#0' : '#')
DropArea { //Drop area for clips
anchors.fill: clipRoot
keys: 'kdenlive/effect'
property string dropData
property string dropSource
property int dropRow: -1
onEntered: {
dropData = drag.getDataAsString('kdenlive/effect')
dropSource = drag.getDataAsString('kdenlive/effectsource')
}
onDropped: {
console.log("Add effect: ", dropData)
if (dropSource == '') {
// drop from effects list
controller.addClipEffect(clipRoot.clipId, dropData);
} else {
controller.copyClipEffect(clipRoot.clipId, dropSource);
}
dropSource = ''
dropRow = -1
drag.acceptProposedAction
}
}
MouseArea {
id: mouseArea
enabled: root.activeTool === 0
anchors.fill: clipRoot
acceptedButtons: Qt.RightButton
hoverEnabled: root.activeTool === 0
cursorShape: (trimInMouseArea.drag.active || trimOutMouseArea.drag.active)? Qt.SizeHorCursor : dragProxyArea.cursorShape
onPressed: {
root.autoScrolling = false
if (mouse.button == Qt.RightButton) {
if (timeline.selection.indexOf(clipRoot.clipId) == -1) {
controller.requestAddToSelection(clipRoot.clipId, true)
}
clipMenu.clipId = clipRoot.clipId
clipMenu.clipStatus = clipRoot.clipStatus
clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor)
clipMenu.grouped = clipRoot.grouped
clipMenu.trackId = clipRoot.trackId
clipMenu.canBeAudio = clipRoot.canBeAudio
clipMenu.canBeVideo = clipRoot.canBeVideo
clipMenu.popup()
}
}
Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down || event.key === Qt.Key_Escape)
Keys.onLeftPressed: {
var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - offset, true, true, true);
}
Keys.onRightPressed: {
var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + offset, true, true, true);
}
Keys.onUpPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
Keys.onDownPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
Keys.onEscapePressed: {
timeline.grabCurrent()
//focus = false
}
onPositionChanged: {
var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
}
onEntered: {
var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
onExited: {
endDrag()
}
onWheel: zoomByWheel(wheel)
Item {
// Thumbs container
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: 0
anchors.topMargin: clipRoot.border.width
anchors.bottomMargin: clipRoot.border.width
clip: true
Loader {
id: thumbsLoader
asynchronous: true
visible: status == Loader.Ready
anchors.fill: parent
source: clipRoot.hideClipViews ? "" : parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : ""
}
}
Item {
// Clipping container
id: container
anchors.fill: parent
anchors.margins: 1.5
clip: true
Rectangle {
// text background
id: labelRect
color: clipRoot.selected ? 'darkred' : '#66000000'
width: label.width + 2
height: label.height
visible: clipRoot.width > width / 2
Text {
id: label
text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '')
font.pointSize: root.fontUnit
anchors {
top: labelRect.top
left: labelRect.left
topMargin: 1
leftMargin: 1
}
color: 'white'
style: Text.Outline
styleColor: 'black'
}
}
Rectangle {
// Offset info
id: offsetRect
color: 'darkgreen'
width: offsetLabel.width + radius
height: offsetLabel.height
radius: height/3
x: labelRect.width + 4
visible: labelRect.visible && positionOffset != 0
MouseArea {
id: offsetArea
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
clearAndMove(positionOffset)
}
ToolTip {
visible: offsetArea.containsMouse
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
font.pointSize: root.fontUnit
text: positionOffset < 0 ? i18n("Offset: -%1", timeline.simplifiedTC(-positionOffset)) : i18n("Offset: %1", timeline.simplifiedTC(positionOffset))
}
}
Text {
id: offsetLabel
text: positionOffset
font.pointSize: root.fontUnit
anchors {
horizontalCenter: parent.horizontalCenter
topMargin: 1
leftMargin: 1
}
color: 'white'
style: Text.Outline
styleColor: 'black'
}
}
}
Rectangle {
// effects
id: effectsRect
color: '#555555'
width: effectLabel.width + 2
height: effectLabel.height
x: labelRect.x
anchors.top: labelRect.bottom
visible: labelRect.visible && clipRoot.effectNames != ''
Text {
id: effectLabel
text: clipRoot.effectNames
font.pointSize: root.fontUnit
anchors {
top: effectsRect.top
left: effectsRect.left
topMargin: 1
leftMargin: 1
// + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1
}
color: 'white'
//style: Text.Outline
styleColor: 'black'
}
}
Repeater {
model: markers
delegate:
Item {
anchors.fill: parent
Rectangle {
id: markerBase
width: 1
height: parent.height
x: clipRoot.speed < 0 ? clipRoot.clipDuration * timeScale + (Math.round(model.frame / clipRoot.speed) - (clipRoot.maxDuration - clipRoot.outPoint)) * timeScale : (Math.round(model.frame / clipRoot.speed) - clipRoot.inPoint) * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: markerBase.x
radius: 2
width: mlabel.width + 4
height: mlabel.height
anchors {
bottom: parent.verticalCenter
}
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onDoubleClicked: timeline.editMarker(clipRoot.clipId, model.frame)
onClicked: proxy.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers && parent.width > width * 1.5
text: model.comment
font.pointSize: root.fontUnit
x: markerBase.x
anchors {
bottom: parent.verticalCenter
topMargin: 2
leftMargin: 2
}
color: 'white'
}
}
}
KeyframeView {
id: effectRow
visible: clipRoot.showKeyframes && clipRoot.keyframeModel
selected: clipRoot.selected
inPoint: clipRoot.inPoint
outPoint: clipRoot.outPoint
masterObject: clipRoot
kfrModel: clipRoot.hideClipViews ? 0 : clipRoot.keyframeModel
}
}
states: [
State {
name: 'locked'
when: isLocked
PropertyChanges {
target: clipRoot
color: root.lockedColor
opacity: 0.8
z: 0
}
},
State {
name: 'normal'
when: clipRoot.selected === false
PropertyChanges {
target: clipRoot
color: Qt.darker(getColor(), 1.5)
z: 0
}
},
State {
name: 'selected'
when: clipRoot.selected === true
PropertyChanges {
target: clipRoot
color: getColor()
z: 3
}
}
]
Rectangle {
id: compositionIn
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
anchors.leftMargin: 4
- width: root.baseUnit * 1.2
+ width: root.baseUnit
height: width
radius: 2
color: Qt.darker('mediumpurple')
border.width: 2
border.color: 'green'
opacity: 0
enabled: !clipRoot.isAudio && dragProxy.draggedItem === clipRoot.clipId
visible: clipRoot.width > 4 * width
MouseArea {
id: compInArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
onPressed: {
timeline.addCompositionToClip('', clipRoot.clipId, 0)
}
onReleased: {
parent.opacity = 0
}
ToolTip {
visible: compInArea.containsMouse && !dragProxyArea.pressed
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
font.pointSize: root.fontUnit
text: i18n("Click to add composition")
}
}
}
}
Rectangle {
id: compositionOut
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
anchors.rightMargin: 4
- width: root.baseUnit * 1.2
+ width: root.baseUnit
height: width
radius: 2
color: Qt.darker('mediumpurple')
border.width: 2
border.color: 'green'
opacity: 0
enabled: !clipRoot.isAudio && dragProxy.draggedItem == clipRoot.clipId
visible: clipRoot.width > 4 * width
MouseArea {
id: compOutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
parent.opacity = 0.7
}
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
onPressed: {
timeline.addCompositionToClip('', clipRoot.clipId, clipRoot.clipDuration - 1)
}
onReleased: {
parent.opacity = 0
}
ToolTip {
visible: compOutArea.containsMouse && !dragProxyArea.pressed
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
font.pointSize: root.fontUnit
text: i18n("Click to add composition")
}
}
}
}
+ }
+
+ TimelineTriangle {
+ id: fadeInTriangle
+ fillColor: 'green'
+ width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width)
+ height: clipRoot.height - clipRoot.border.width * 2
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.margins: clipRoot.border.width
+ opacity: 0.3
+ }
+ TimelineTriangle {
+ id: fadeOutCanvas
+ fillColor: 'red'
+ width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width)
+ height: clipRoot.height - clipRoot.border.width * 2
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: clipRoot.border.width
+ opacity: 0.3
+ transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2}
+ }
- TimelineTriangle {
- id: fadeInTriangle
- fillColor: 'green'
- width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width)
- height: clipRoot.height - clipRoot.border.width * 2
- anchors.left: parent.left
- anchors.top: parent.top
- anchors.margins: clipRoot.border.width
- opacity: 0.3
+ MouseArea {
+ id: trimInMouseArea
+ anchors.left: clipRoot.left
+ anchors.leftMargin: 0
+ height: parent.height
+ width: root.baseUnit / 2
+ enabled: !isLocked
+ hoverEnabled: true
+ drag.target: trimInMouseArea
+ drag.axis: Drag.XAxis
+ drag.smoothed: false
+ property bool shiftTrim: false
+ property bool controlTrim: false
+ property bool sizeChanged: false
+ cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
+ onPressed: {
+ root.autoScrolling = false
+ clipRoot.originalX = clipRoot.x
+ clipRoot.originalDuration = clipDuration
+ anchors.left = undefined
+ shiftTrim = mouse.modifiers & Qt.ShiftModifier
+ controlTrim = mouse.modifiers & Qt.ControlModifier
+ if (!shiftTrim && clipRoot.grouped) {
+ clipRoot.initGroupTrim(clipRoot)
+ }
+ trimIn.opacity = 0
+ }
+ onReleased: {
+ root.autoScrolling = timeline.autoScroll
+ anchors.left = clipRoot.left
+ if (sizeChanged) {
+ clipRoot.trimmedIn(clipRoot, shiftTrim, controlTrim)
+ sizeChanged = false
+ }
}
- Rectangle {
- id: fadeInControl
- anchors.left: fadeInTriangle.right
- anchors.leftMargin: fadeInTriangle.width > root.baseUnit ? -root.baseUnit : 0
- anchors.top: fadeInTriangle.top
- anchors.topMargin: -10
- width: root.baseUnit * 2
- height: width
- radius: width / 2
- color: '#FF66FFFF'
- border.width: 2
- border.color: 'green'
- enabled: !isLocked && !dragProxy.isComposition
- opacity: 0
- visible : clipRoot.width > 3 * width
- Drag.active: fadeInMouseArea.drag.active
- MouseArea {
- id: fadeInMouseArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- drag.target: parent
- drag.minimumX: -root.baseUnit
- drag.maximumX: container.width
- drag.axis: Drag.XAxis
- drag.smoothed: false
- property int startX
- property int startFadeIn
- onEntered: parent.opacity = 0.7
- onExited: {
- if (!pressed) {
- parent.opacity = 0
- }
- }
- onPressed: {
- root.autoScrolling = false
- startX = Math.round(parent.x / timeScale)
- startFadeIn = clipRoot.fadeIn
- parent.anchors.left = undefined
- parent.opacity = 1
- fadeInTriangle.opacity = 0.5
- // parentTrack.clipSelected(clipRoot, parentTrack) TODO
- }
- onReleased: {
- root.autoScrolling = timeline.autoScroll
- fadeInTriangle.opacity = 0.3
- parent.opacity = 0
- parent.anchors.left = fadeInTriangle.right
- console.log('released fade: ', clipRoot.fadeIn)
- timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn)
- bubbleHelp.hide()
- }
- onPositionChanged: {
- if (mouse.buttons === Qt.LeftButton) {
- var delta = Math.round(parent.x / timeScale) - startX
- var duration = Math.max(0, startFadeIn + delta)
- duration = Math.min(duration, clipRoot.clipDuration - 1)
- if (duration != clipRoot.fadeIn) {
- timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
- // Show fade duration as time in a "bubble" help.
- var s = timeline.simplifiedTC(Math.max(duration, 0))
- bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s)
- }
+ onPositionChanged: {
+ if (mouse.buttons === Qt.LeftButton) {
+ var delta = Math.round(x / timeScale)
+ if (delta !== 0) {
+ if (maxDuration > 0 && delta < -inPoint) {
+ delta = -inPoint
}
+ var newDuration = clipDuration - delta
+ sizeChanged = true
+ clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim, controlTrim)
}
}
- SequentialAnimation on scale {
- loops: Animation.Infinite
- running: fadeInMouseArea.containsMouse && !fadeInMouseArea.pressed
- NumberAnimation {
- from: 1.0
- to: 0.7
- duration: 250
- easing.type: Easing.InOutQuad
- }
- NumberAnimation {
- from: 0.7
- to: 1.0
- duration: 250
- easing.type: Easing.InOutQuad
- }
+ }
+ onEntered: {
+ if (!pressed) {
+ trimIn.opacity = 0.5
}
}
-
- TimelineTriangle {
- id: fadeOutCanvas
- fillColor: 'red'
- width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width)
- height: clipRoot.height - clipRoot.border.width * 2
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.margins: clipRoot.border.width
- opacity: 0.3
- transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2}
+ onExited: {
+ trimIn.opacity = 0
}
Rectangle {
- id: fadeOutControl
- anchors.right: fadeOutCanvas.left
- anchors.rightMargin: fadeOutCanvas.width > root.baseUnit ? -root.baseUnit : 0
- anchors.top: fadeOutCanvas.top
- anchors.topMargin: -10
- width: root.baseUnit * 2
- height: width
- radius: width / 2
- color: '#66FFFFFF'
- border.width: 2
- border.color: 'red'
+ id: trimIn
+ anchors.fill: parent
+ anchors.margins: 2
+ color: isAudio? 'green' : 'lawngreen'
opacity: 0
- enabled: !isLocked && !dragProxy.isComposition
- Drag.active: fadeOutMouseArea.drag.active
- visible : clipRoot.width > 3 * width
- MouseArea {
- id: fadeOutMouseArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- drag.target: parent
- drag.axis: Drag.XAxis
- drag.minimumX: -root.baseUnit
- drag.maximumX: container.width
- property int startX
- property int startFadeOut
- onEntered: parent.opacity = 0.7
- onExited: {
- if (!pressed) {
- parent.opacity = 0
- }
- }
- drag.smoothed: false
- onPressed: {
- root.autoScrolling = false
- startX = Math.round(parent.x / timeScale)
- startFadeOut = clipRoot.fadeOut
- parent.anchors.right = undefined
- parent.opacity = 1
- fadeOutCanvas.opacity = 0.5
- }
- onReleased: {
- fadeOutCanvas.opacity = 0.3
- parent.opacity = 0
- root.autoScrolling = timeline.autoScroll
- parent.anchors.right = fadeOutCanvas.left
- var duration = clipRoot.fadeOut
- if (duration > 0) {
- duration += 1
- }
- timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, startFadeOut)
- bubbleHelp.hide()
- }
- onPositionChanged: {
- if (mouse.buttons === Qt.LeftButton) {
- var delta = startX - Math.round(parent.x / timeScale)
- var duration = Math.max(0, startFadeOut + delta)
- duration = Math.min(duration, clipRoot.clipDuration)
- if (clipRoot.fadeOut != duration) {
- timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
- // Show fade duration as time in a "bubble" help.
- var s = timeline.simplifiedTC(Math.max(duration, 0))
- bubbleHelp.show(clipRoot.x + clipRoot.width, parentTrack.y + clipRoot.height, s)
- }
- }
- }
- }
- SequentialAnimation on scale {
- loops: Animation.Infinite
- running: fadeOutMouseArea.containsMouse && !fadeOutMouseArea.pressed
- NumberAnimation {
- from: 1.0
- to: 0.7
- duration: 250
- easing.type: Easing.InOutQuad
- }
- NumberAnimation {
- from: 0.7
- to: 1.0
- duration: 250
- easing.type: Easing.InOutQuad
+ Drag.active: trimInMouseArea.drag.active
+ Drag.proposedAction: Qt.MoveAction
+ visible: trimInMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width)
+
+ ToolTip {
+ visible: trimInMouseArea.containsMouse && !trimInMouseArea.pressed
+ delay: 1000
+ timeout: 5000
+ background: Rectangle {
+ color: activePalette.alternateBase
+ border.color: activePalette.light
+ }
+ contentItem: Label {
+ color: activePalette.text
+ font.pointSize: root.fontUnit
+ text: i18n("In:%1\nPosition:%2", timeline.simplifiedTC(clipRoot.inPoint),timeline.simplifiedTC(clipRoot.modelStart))
}
}
}
}
- Rectangle {
- id: trimIn
- anchors.left: clipRoot.left
- anchors.leftMargin: 0
+ MouseArea {
+ id: trimOutMouseArea
+ anchors.right: clipRoot.right
height: parent.height
- enabled: !isLocked
width: root.baseUnit / 2
- color: isAudio? 'green' : 'lawngreen'
- opacity: 0
- Drag.active: trimInMouseArea.drag.active
- Drag.proposedAction: Qt.MoveAction
- visible: trimInMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width)
+ hoverEnabled: true
+ enabled: !isLocked
+ property bool shiftTrim: false
+ property bool controlTrim: false
+ property bool sizeChanged: false
+ cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
+ drag.target: trimOutMouseArea
+ drag.axis: Drag.XAxis
+ drag.smoothed: false
+ onPressed: {
+ root.autoScrolling = false
+ clipRoot.originalDuration = clipDuration
+ anchors.right = undefined
+ shiftTrim = mouse.modifiers & Qt.ShiftModifier
+ controlTrim = mouse.modifiers & Qt.ControlModifier
+ if (!shiftTrim && clipRoot.grouped) {
+ clipRoot.initGroupTrim(clipRoot)
+ }
+ trimOut.opacity = 0
+ }
+ onReleased: {
+ root.autoScrolling = timeline.autoScroll
+ anchors.right = clipRoot.right
+ if (sizeChanged) {
+ clipRoot.trimmedOut(clipRoot, shiftTrim, controlTrim)
+ sizeChanged = false
+ }
+ }
+ onPositionChanged: {
+ if (mouse.buttons === Qt.LeftButton) {
+ var newDuration = Math.round((x + width) / timeScale)
+ if (maxDuration > 0 && newDuration > maxDuration - inPoint) {
+ newDuration = maxDuration - inPoint
+ }
+ if (newDuration != clipDuration) {
+ sizeChanged = true
+ clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim, controlTrim)
+ }
+ }
+ }
+ onEntered: {
+ if (!pressed) {
+ trimOut.opacity = 0.5
+ }
+ }
+ onExited: trimOut.opacity = 0
+
ToolTip {
- visible: trimInMouseArea.containsMouse && !trimInMouseArea.pressed
+ visible: trimOutMouseArea.containsMouse && !trimOutMouseArea.pressed
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
font.pointSize: root.fontUnit
- text: i18n("In:%1\nPosition:%2", timeline.simplifiedTC(clipRoot.inPoint),timeline.simplifiedTC(clipRoot.modelStart))
+ text: i18n("Out: ") + timeline.simplifiedTC(clipRoot.outPoint)
}
}
-
- MouseArea {
- id: trimInMouseArea
+ Rectangle {
+ id: trimOut
anchors.fill: parent
+ anchors.margins: 2
+ color: 'red'
+ opacity: 0
+ Drag.active: trimOutMouseArea.drag.active
+ Drag.proposedAction: Qt.MoveAction
+ visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width)
+ }
+ }
+
+ MouseArea {
+ id: fadeOutMouseArea
+ anchors.right: fadeOutCanvas.left
+ anchors.rightMargin: -width/2
+ anchors.top: fadeOutCanvas.top
+ anchors.topMargin: -width / 3
+ width: root.baseUnit
+ height: width
+ //anchors.fill: parent
hoverEnabled: true
- drag.target: parent
+ cursorShape: Qt.PointingHandCursor
+ drag.target: fadeOutMouseArea
drag.axis: Drag.XAxis
+ drag.minimumX: - Math.ceil(width / 2)
+ drag.maximumX: container.width
+ visible : clipRoot.width > 3 * width
+ property int startFadeOut
+ property int lastDuration: -1
+ onEntered: fadeOutControl.opacity = 0.7
+ onExited: {
+ if (!pressed) {
+ fadeOutControl.opacity = 0
+ }
+ }
drag.smoothed: false
- property bool shiftTrim: false
- property bool controlTrim: false
- property bool sizeChanged: false
- cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
onPressed: {
root.autoScrolling = false
- clipRoot.originalX = clipRoot.x
- clipRoot.originalDuration = clipDuration
- parent.anchors.left = undefined
- shiftTrim = mouse.modifiers & Qt.ShiftModifier
- controlTrim = mouse.modifiers & Qt.ControlModifier
- if (!shiftTrim && clipRoot.grouped) {
- clipRoot.initGroupTrim(clipRoot)
- }
- parent.opacity = 0
+ startFadeOut = clipRoot.fadeOut
+ anchors.right = undefined
+ fadeOutControl.opacity = 1
}
onReleased: {
+ fadeOutCanvas.opacity = 0.3
root.autoScrolling = timeline.autoScroll
- parent.anchors.left = clipRoot.left
- if (sizeChanged) {
- clipRoot.trimmedIn(clipRoot, shiftTrim, controlTrim)
- sizeChanged = false
+ anchors.right = fadeOutCanvas.left
+ if (!fadeOutMouseArea.containsMouse) {
+ fadeOutControl.opacity = 0
+ }
+ var duration = clipRoot.fadeOut
+ if (duration > 0) {
+ duration += 1
}
+ timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, startFadeOut)
+ bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
- var delta = Math.round((trimIn.x) / timeScale)
- if (delta !== 0) {
- if (maxDuration > 0 && delta < -inPoint) {
- delta = -inPoint
- }
- var newDuration = clipDuration - delta
- sizeChanged = true
- clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim, controlTrim)
+ var delta = clipRoot.clipDuration - Math.floor(x / timeScale)
+ var duration = Math.max(0, delta)
+ duration = Math.min(duration, clipRoot.clipDuration)
+ if (lastDuration != duration) {
+ lastDuration = duration
+ timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
+ // Show fade duration as time in a "bubble" help.
+ var s = timeline.simplifiedTC(clipRoot.fadeOut + (clipRoot.fadeOut > 0 ? 1 : 0))
+ bubbleHelp.show(clipRoot.x + x, parentTrack.y + parentTrack.height, s)
}
}
}
- onEntered: {
- if (!pressed) {
- parent.opacity = 0.5
+ Rectangle {
+ id: fadeOutControl
+ anchors.fill: parent
+ radius: width / 2
+ color: '#66FFFFFF'
+ border.width: 2
+ border.color: 'red'
+ opacity: 0
+ enabled: !isLocked && !dragProxy.isComposition
+ Drag.active: fadeOutMouseArea.drag.active
+ Rectangle {
+ id: fadeOutMarker
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: parent.width / 3
+ color: 'red'
+ height: container.height - 1
+ width: 1
}
}
- onExited: {
- parent.opacity = 0
- }
}
- }
- Rectangle {
- id: trimOut
- anchors.right: clipRoot.right
- anchors.rightMargin: 0
- height: parent.height
- width: root.baseUnit / 2
- color: 'red'
- opacity: 0
- enabled: !isLocked
- Drag.active: trimOutMouseArea.drag.active
- Drag.proposedAction: Qt.MoveAction
- visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width)
-
- ToolTip {
- visible: trimOutMouseArea.containsMouse && !trimOutMouseArea.pressed
- delay: 1000
- timeout: 5000
- background: Rectangle {
- color: activePalette.alternateBase
- border.color: activePalette.light
- }
- contentItem: Label {
- color: activePalette.text
- font.pointSize: root.fontUnit
- text: i18n("Out: ") + timeline.simplifiedTC(clipRoot.outPoint)
- }
- }
-
+
MouseArea {
- id: trimOutMouseArea
- anchors.fill: parent
+ id: fadeInMouseArea
+ anchors.left: fadeInTriangle.right
+ anchors.leftMargin: -width / 2
+ anchors.top: fadeInTriangle.top
+ anchors.topMargin: -width / 3
+ width: root.baseUnit
+ height: width
hoverEnabled: true
- property bool shiftTrim: false
- property bool controlTrim: false
- property bool sizeChanged: false
- cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
- drag.target: parent
+ cursorShape: Qt.PointingHandCursor
+ drag.target: fadeInMouseArea
+ drag.minimumX: - Math.ceil(width / 2)
+ drag.maximumX: container.width - width / 2
drag.axis: Drag.XAxis
drag.smoothed: false
-
+ property int startFadeIn
+ onEntered: fadeInControl.opacity = 0.7
+ onExited: {
+ if (!pressed) {
+ fadeInControl.opacity = 0
+ }
+ }
onPressed: {
root.autoScrolling = false
- clipRoot.originalDuration = clipDuration
- parent.anchors.right = undefined
- shiftTrim = mouse.modifiers & Qt.ShiftModifier
- controlTrim = mouse.modifiers & Qt.ControlModifier
- if (!shiftTrim && clipRoot.grouped) {
- clipRoot.initGroupTrim(clipRoot)
- }
- parent.opacity = 0
+ startFadeIn = clipRoot.fadeIn
+ anchors.left = undefined
+ fadeInControl.opacity = 1
+ fadeInTriangle.opacity = 0.5
+ // parentTrack.clipSelected(clipRoot, parentTrack) TODO
}
onReleased: {
root.autoScrolling = timeline.autoScroll
- parent.anchors.right = clipRoot.right
- if (sizeChanged) {
- clipRoot.trimmedOut(clipRoot, shiftTrim, controlTrim)
- sizeChanged = false
- }
+ fadeInTriangle.opacity = 0.3
+ if (!fadeInMouseArea.containsMouse) {
+ fadeInControl.opacity = 0
+ }
+ anchors.left = fadeInTriangle.right
+ console.log('released fade: ', clipRoot.fadeIn)
+ timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn)
+ bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
- var newDuration = Math.round((parent.x + parent.width) / timeScale)
- if (maxDuration > 0 && newDuration > maxDuration - inPoint) {
- newDuration = maxDuration - inPoint
- }
- if (newDuration != clipDuration) {
- sizeChanged = true
- clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim, controlTrim)
+ var delta = Math.round(x / timeScale)
+ var duration = Math.max(0, delta)
+ duration = Math.min(duration, clipRoot.clipDuration - 1)
+ if (duration != clipRoot.fadeIn) {
+ timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
+ // Show fade duration as time in a "bubble" help.
+ var s = timeline.simplifiedTC(clipRoot.fadeIn)
+ bubbleHelp.show(clipRoot.x + x, parentTrack.y + parentTrack.height, s)
}
}
}
- onEntered: {
- if (!pressed) {
- parent.opacity = 0.5
+ Rectangle {
+ id: fadeInControl
+ anchors.fill: parent
+ radius: width / 2
+ color: '#FF66FFFF'
+ border.width: 2
+ border.color: 'green'
+ enabled: !isLocked && !dragProxy.isComposition
+ opacity: 0
+ visible : clipRoot.width > 3 * width
+ Drag.active: fadeInMouseArea.drag.active
+ Rectangle {
+ id: fadeInMarker
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: parent.width / 3
+ color: 'green'
+ height: container.height - 1
+ width: 1
}
}
- onExited: parent.opacity = 0
}
- }
/*MenuItem {
id: mergeItem
text: i18n("Merge with next clip")
onTriggered: timeline.mergeClipWithNext(trackIndex, index, false)
}
MenuItem {
text: i18n("Rebuild Audio Waveform")
onTriggered: timeline.remakeAudioLevels(trackIndex, index)
}*/
/*onPopupVisibleChanged: {
if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) {
// Try to fix menu running off screen. This only works intermittently.
menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40))
menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width))
}
}*/
}
diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml
index 4683863c4..7ae4b3484 100644
--- a/src/timeline2/view/qml/timeline.qml
+++ b/src/timeline2/view/qml/timeline.qml
@@ -1,1532 +1,1532 @@
import QtQuick 2.11
import QtQml.Models 2.11
import QtQuick.Controls 1.4 as OLD
import QtQuick.Controls.Styles 1.4
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
import QtQuick.Dialogs 1.3
import Kdenlive.Controls 1.0
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 + ruler.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.pixelSize
property real fontUnit: fontMetrics.font.pointSize * 0.8
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 consumerPosition: proxy.position
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
property bool seekingFinished : proxy.seekFinished
property int scrollMin: scrollView.flickableItem.contentX / timeline.scaleFactor
property int scrollMax: scrollMin + scrollView.viewport.width / timeline.scaleFactor
onSeekingFinishedChanged : {
playhead.opacity = seekingFinished ? 1 : 0.5
}
//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, root.consumerPosition * 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()
}
}
onConsumerPositionChanged: {
if (autoScrolling) Logic.scrollIfNeeded()
}
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, root.consumerPosition, 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, root.consumerPosition, 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, root.consumerPosition, 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(root.consumerPosition);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
proxy.position = assetFrame
}
}
OLD.MenuItem {
id: editGuideMenu
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(root.consumerPosition);
}
}
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(root.consumerPosition)) {
// 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(root.consumerPosition);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
proxy.position = assetFrame
}
}
OLD.MenuItem {
id: editGuideMenu2
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(root.consumerPosition);
}
}
OLD.MenuItem {
id: addProjectNote
text: i18n("Add Project Note")
onTriggered: {
timeline.triggerAction('add_project_note')
}
}
onAboutToShow: {
if (guidesModel.hasMarker(root.consumerPosition)) {
// 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
width: headerWidth
z: 1
Item {
// Padding between toolbar and track headers.
width: parent.width
height: ruler.height
Button {
text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : "M"
anchors.fill: parent
anchors.leftMargin: 2
anchors.rightMargin: 2
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: i18n("Show master effects")
TextMetrics {
id: metrics
text: i18n("Master")
}
onClicked: {
timeline.showMasterEffects()
}
}
}
Flickable {
// Non-slider scroll area for the track headers.
id: headerFlick
contentY: scrollView.flickableItem.contentY
width: parent.width
y: ruler.height
height: root.height - ruler.height
interactive: false
clip: true
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 + ruler.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)
}
}
}
}
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 - root.headerWidth
height: root.height
x: root.headerWidth
// This provides continuous scrubbing and scimming at the left/right edges.
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
cursorShape: 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
proxy.position = wheel.angleDelta.y > 0 ? Math.max(root.consumerPosition - delta, 0) : Math.min(root.consumerPosition + delta, timeline.fullDuration - 1)
}
}
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
if (y >= 0) {
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();
}
proxy.position = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
}
} 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) {
proxy.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) {
proxy.position = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
} 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, root.consumerPosition, 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
proxy.position = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
} 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: root.baseUnit * 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: proxy.seekPosition > -1
color: activePalette.highlight
width: 4
height: ruler.height
opacity: 0.5
x: proxy.seekPosition * timeline.scaleFactor
}*/
TimelinePlayhead {
id: playhead
height: root.baseUnit * .8
width: root.baseUnit * 1.2
fillColor: activePalette.windowText
anchors.bottom: parent.bottom
x: root.consumerPosition * timeline.scaleFactor - (width / 2)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: ruler.resizeActive ? Qt.SizeHorCursor : 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 = Math.max(0, 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, root.consumerPosition, 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, root.consumerPosition, 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: root.consumerPosition > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: root.consumerPosition * 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: root.consumerPosition * 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
+ width: bubbleHelpLabel.width + 6
+ height: bubbleHelpLabel.height + 6
+ radius: 3
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.pointSize: root.fontUnit
}
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
+ bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelp.width
+ bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelp.height + ruler.height - 3
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: proxy.position = guideBase.x / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers
text: model.comment
font.pointSize: root.fontUnit
x: guideBase.x + 2
y: scrollView.flickableItem.contentY
color: 'white'
}
}
}
Connections {
target: timeline
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()
}
}
}
diff --git a/src/timeline2/view/timelinewidget.cpp b/src/timeline2/view/timelinewidget.cpp
index 86fab3cba..ffdfa7cfd 100644
--- a/src/timeline2/view/timelinewidget.cpp
+++ b/src/timeline2/view/timelinewidget.cpp
@@ -1,236 +1,238 @@
/***************************************************************************
* Copyright (C) 2017 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinewidget.h"
#include "../model/builders/meltBuilder.hpp"
#include "assets/keyframes/model/keyframemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "capture/mediacapture.h"
#include "core.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "effects/effectlist/model/effectfilter.hpp"
#include "effects/effectlist/model/effecttreemodel.hpp"
#include "kdenlivesettings.h"
#include "mainwindow.h"
#include "profiles/profilemodel.hpp"
#include "project/projectmanager.h"
#include "monitor/monitorproxy.h"
#include "qml/timelineitems.h"
#include "qmltypes/thumbnailprovider.h"
#include "timelinecontroller.h"
#include "transitions/transitionlist/model/transitionfilter.hpp"
#include "transitions/transitionlist/model/transitiontreemodel.hpp"
#include "utils/clipboardproxy.hpp"
#include
// #include
#include
#include
#include
#include
#include
+#include
#include
const int TimelineWidget::comboScale[] = {1, 2, 4, 8, 15, 30, 50, 75, 100, 150, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 6000, 15000, 30000};
TimelineWidget::TimelineWidget(QWidget *parent)
: QQuickWidget(parent)
{
KDeclarative::KDeclarative kdeclarative;
kdeclarative.setDeclarativeEngine(engine());
kdeclarative.setupEngine(engine());
kdeclarative.setupContext();
setClearColor(palette().window().color());
registerTimelineItems();
// Build transition model for context menu
m_transitionModel = TransitionTreeModel::construct(true, this);
m_transitionProxyModel = std::make_unique(this);
static_cast(m_transitionProxyModel.get())->setFilterType(true, TransitionType::Favorites);
m_transitionProxyModel->setSourceModel(m_transitionModel.get());
m_transitionProxyModel->setSortRole(AssetTreeModel::NameRole);
m_transitionProxyModel->sort(0, Qt::AscendingOrder);
// Build effects model for context menu
m_effectsModel = EffectTreeModel::construct(QStringLiteral(), this);
m_effectsProxyModel = std::make_unique(this);
static_cast(m_effectsProxyModel.get())->setFilterType(true, EffectType::Favorites);
m_effectsProxyModel->setSourceModel(m_effectsModel.get());
m_effectsProxyModel->setSortRole(AssetTreeModel::NameRole);
m_effectsProxyModel->sort(0, Qt::AscendingOrder);
m_proxy = new TimelineController(this);
connect(m_proxy, &TimelineController::zoneMoved, this, &TimelineWidget::zoneMoved);
connect(m_proxy, &TimelineController::ungrabHack, this, &TimelineWidget::slotUngrabHack);
setResizeMode(QQuickWidget::SizeRootObjectToView);
m_thumbnailer = new ThumbnailProvider;
engine()->addImageProvider(QStringLiteral("thumbnail"), m_thumbnailer);
setVisible(false);
+ setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
setFocusPolicy(Qt::StrongFocus);
}
TimelineWidget::~TimelineWidget()
{
delete m_proxy;
}
void TimelineWidget::updateEffectFavorites()
{
rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false));
}
void TimelineWidget::updateTransitionFavorites()
{
rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true));
}
const QStringList TimelineWidget::sortedItems(const QStringList &items, bool isTransition)
{
QMap sortedItems;
for (const QString &effect : items) {
sortedItems.insert(m_proxy->getAssetName(effect, isTransition), effect);
}
return sortedItems.values();
}
void TimelineWidget::setModel(const std::shared_ptr &model, MonitorProxy *proxy)
{
m_sortModel = std::make_unique(this);
m_sortModel->setSourceModel(model.get());
m_sortModel->setSortRole(TimelineItemModel::SortRole);
m_sortModel->sort(0, Qt::DescendingOrder);
m_proxy->setModel(model);
rootContext()->setContextProperty("multitrack", m_sortModel.get());
rootContext()->setContextProperty("controller", model.get());
rootContext()->setContextProperty("timeline", m_proxy);
rootContext()->setContextProperty("proxy", proxy);
// Create a unique id for this timeline to prevent thumbnails
// leaking from one project to another because of qml's image caching
rootContext()->setContextProperty("documentId", QUuid::createUuid());
rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); // m_transitionProxyModel.get());
// rootContext()->setContextProperty("effectModel", m_effectsProxyModel.get());
rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false));
rootContext()->setContextProperty("audiorec", pCore->getAudioDevice());
rootContext()->setContextProperty("guidesModel", pCore->projectManager()->current()->getGuideModel().get());
rootContext()->setContextProperty("clipboard", new ClipboardProxy(this));
setSource(QUrl(QStringLiteral("qrc:/qml/timeline.qml")));
connect(rootObject(), SIGNAL(mousePosChanged(int)), pCore->window(), SLOT(slotUpdateMousePosition(int)));
connect(rootObject(), SIGNAL(zoomIn(bool)), pCore->window(), SLOT(slotZoomIn(bool)));
connect(rootObject(), SIGNAL(zoomOut(bool)), pCore->window(), SLOT(slotZoomOut(bool)));
connect(rootObject(), SIGNAL(processingDrag(bool)), pCore->window(), SIGNAL(enableUndo(bool)));
connect(m_proxy, &TimelineController::seeked, proxy, &MonitorProxy::setPosition);
m_proxy->setRoot(rootObject());
setVisible(true);
loading = false;
m_proxy->checkDuration();
}
void TimelineWidget::mousePressEvent(QMouseEvent *event)
{
emit focusProjectMonitor();
QQuickWidget::mousePressEvent(event);
}
void TimelineWidget::slotChangeZoom(int value, bool zoomOnMouse)
{
double pixelScale = QFontMetrics(font()).maxWidth() * 2;
m_proxy->setScaleFactorOnMouse(pixelScale / comboScale[value], zoomOnMouse);
}
void TimelineWidget::slotFitZoom()
{
QVariant returnedValue;
double prevScale = m_proxy->scaleFactor();
QMetaObject::invokeMethod(rootObject(), "fitZoom", Q_RETURN_ARG(QVariant, returnedValue));
double scale = returnedValue.toDouble();
QMetaObject::invokeMethod(rootObject(), "scrollPos", Q_RETURN_ARG(QVariant, returnedValue));
int scrollPos = returnedValue.toInt();
if (qFuzzyCompare(prevScale, scale)) {
scale = m_prevScale;
scrollPos = m_scrollPos;
} else {
m_prevScale = prevScale;
m_scrollPos = scrollPos;
scrollPos = 0;
}
m_proxy->setScaleFactorOnMouse(scale, false);
// Update zoom slider
m_proxy->updateZoom(scale);
QMetaObject::invokeMethod(rootObject(), "goToStart", Q_ARG(QVariant, scrollPos));
}
Mlt::Tractor *TimelineWidget::tractor()
{
return m_proxy->tractor();
}
TimelineController *TimelineWidget::controller()
{
return m_proxy;
}
std::shared_ptr TimelineWidget::model()
{
return m_proxy->getModel();
}
void TimelineWidget::zoneUpdated(const QPoint &zone)
{
m_proxy->setZone(zone);
}
void TimelineWidget::setTool(ProjectTool tool)
{
rootObject()->setProperty("activeTool", (int)tool);
}
QPoint TimelineWidget::getTracksCount() const
{
return m_proxy->getTracksCount();
}
void TimelineWidget::slotUngrabHack()
{
// Workaround bug: https://bugreports.qt.io/browse/QTBUG-59044
// https://phabricator.kde.org/D5515
if (quickWindow() && quickWindow()->mouseGrabberItem()) {
quickWindow()->mouseGrabberItem()->ungrabMouse();
}
}
int TimelineWidget::zoomForScale(double value) const
{
int scale = 100.0 / value;
int ix = 13;
while (comboScale[ix] > scale && ix > 0) {
ix--;
}
return ix;
}
void TimelineWidget::focusTimeline()
{
setFocus();
if (rootObject()) {
rootObject()->setFocus(true);
}
}