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); } }