diff --git a/Messages.sh b/Messages.sh index b065c3d46..2372d6c56 100755 --- a/Messages.sh +++ b/Messages.sh @@ -1,9 +1,9 @@ -#!bin/sh +#!/bin/sh kdenlive_subdirs="plugins renderer data src src/ui" $EXTRACTRC --tag=name --tag=description --tag=label --tag=comment --tag=paramlistdisplay data/effects/*.xml data/kdenliveeffectscategory.rc >> rc.cpp $EXTRACTRC `find $kdenlive_subdirs -name \*.rc -a ! -name encodingprofiles.rc -a ! -name camcorderfilters.rc -o -name \*.ui` >> rc.cpp -$XGETTEXT `find $kdenlive_subdirs -name \*.cpp -o -name \*.h` *.cpp -o $podir/kdenlive.pot +$XGETTEXT `find $kdenlive_subdirs -name \*.cpp -o -name \*.h -o -name \*.qml` *.cpp -o $podir/kdenlive.pot rm -f rc.cpp diff --git a/src/assets/assetlist/view/qml/assetList.qml b/src/assets/assetlist/view/qml/assetList.qml index dd6f76922..efdd93001 100644 --- a/src/assets/assetlist/view/qml/assetList.qml +++ b/src/assets/assetlist/view/qml/assetList.qml @@ -1,382 +1,382 @@ /*************************************************************************** * 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 . * ***************************************************************************/ import QtQuick 2.7 import QtQuick.Layouts 1.3 import QtQuick.Controls 1.5 import QtQuick.Controls.Styles 1.4 import QtQuick.Window 2.2 import QtQml.Models 2.2 Rectangle { id: listRoot SystemPalette { id: activePalette } color: activePalette.window function assetType(){ return isEffectList ? i18n("effects") : i18n("compositions"); } function expandNodes(indexes) { for(var i = 0; i < indexes.length; i++) { if (indexes[i].valid) { treeView.expand(indexes[i]); } } } function rowPosition(model, index) { var pos = 0; for(var i = 0; i < index.parent.row; i++) { var catIndex = model.getCategory(i); if (treeView.isExpanded(catIndex)) { pos += model.rowCount(catIndex); } pos ++; } pos += index.row + 2; return pos; } ColumnLayout { anchors.fill: parent spacing: 0 RowLayout { Layout.fillWidth: true Layout.fillHeight: false spacing: 4 ExclusiveGroup { id: filterGroup} ToolButton { id: searchList iconName: "edit-find" checkable: true - tooltip: isEffectList ? i18n('Find effect') : i18n('Find composition') + tooltip: isEffectList ? i18n("Find effect") : i18n("Find composition") onCheckedChanged: { searchInput.visible = searchList.checked searchInput.focus = searchList.checked if (!searchList.checked) { searchInput.text = '' treeView.focus = true } } } ToolButton { id: showAll iconName: "show-all-effects" checkable: true checked: true exclusiveGroup: filterGroup - tooltip: i18n('Main %1', assetType()) + tooltip: i18n("Main %1", assetType()) onClicked: { assetlist.setFilterType("") } } ToolButton { id: showVideo visible: isEffectList iconName: "kdenlive-show-video" iconSource: 'qrc:///pics/kdenlive-show-video.svgz' checkable:true exclusiveGroup: filterGroup - tooltip: i18n('Show all video effects') + tooltip: i18n("Show all video effects") onClicked: { assetlist.setFilterType("video") } } ToolButton { id: showAudio visible: isEffectList iconName: "kdenlive-show-audio" iconSource: 'qrc:///pics/kdenlive-show-audio.svgz' checkable:true exclusiveGroup: filterGroup - tooltip: i18n('Show all audio effects') + tooltip: i18n("Show all audio effects") onClicked: { assetlist.setFilterType("audio") } } ToolButton { id: showCustom visible: isEffectList iconName: "kdenlive-custom-effect" checkable:true exclusiveGroup: filterGroup - tooltip: i18n('Show all custom effects') + tooltip: i18n("Show all custom effects") onClicked: { assetlist.setFilterType("custom") } } ToolButton { id: showFavorites iconName: "favorite" checkable:true exclusiveGroup: filterGroup - tooltip: i18n('Show favorite items') + tooltip: i18n("Show favorite items") onClicked: { assetlist.setFilterType("favorites") } } ToolButton { id: downloadTransitions visible: !isEffectList iconName: "edit-download" - tooltip: i18n('Download New Wipes...') + tooltip: i18n("Download New Wipes...") onClicked: { assetlist.downloadNewLumas() } } Rectangle { //This is a spacer Layout.fillHeight: false Layout.fillWidth: true color: "transparent" } ToolButton { id: showDescription iconName: "help-about" checkable:true - tooltip: i18n('Show/hide description of the ') + assetType() + tooltip: i18n("Show/hide description of the %1", assetType()) onCheckedChanged:{ assetlist.showDescription = checked } Component.onCompleted: checked = assetlist.showDescription } } TextField { id: searchInput Layout.fillWidth:true visible: false Image { id: clear source: 'image://icon/edit-clear' width: parent.height * 0.8 height: width anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } opacity: 0 MouseArea { anchors.fill: parent onClicked: { searchInput.text = ''; searchInput.focus = true; searchList.checked = false; } } } states: State { name: "hasText"; when: searchInput.text != '' PropertyChanges { target: clear; opacity: 1 } } transitions: [ Transition { from: ""; to: "hasText" NumberAnimation { properties: "opacity" } }, Transition { from: "hasText"; to: "" NumberAnimation { properties: "opacity" } } ] onTextChanged: { var current = sel.currentIndex var rowModelIndex = assetListModel.getModelIndex(sel.currentIndex); assetlist.setFilterName(text) if (text.length > 0) { sel.setCurrentIndex(assetListModel.firstVisibleItem(current), ItemSelectionModel.ClearAndSelect) } else { sel.clearCurrentIndex() sel.setCurrentIndex(assetListModel.getProxyIndex(rowModelIndex), ItemSelectionModel.ClearAndSelect) } treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible) } /*onEditingFinished: { if (!assetContextMenu.isDisplayed) { searchList.checked = false } }*/ Keys.onDownPressed: { sel.setCurrentIndex(assetListModel.getNextChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect) treeView.expand(sel.currentIndex.parent) treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible) } Keys.onUpPressed: { sel.setCurrentIndex(assetListModel.getPreviousChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect) treeView.expand(sel.currentIndex.parent) treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible) } Keys.onReturnPressed: { if (sel.hasSelection) { assetlist.activate(sel.currentIndex) searchList.checked = false } } } ItemSelectionModel { id: sel model: assetListModel onSelectionChanged: { assetDescription.text = assetlist.getDescription(sel.currentIndex) } } SplitView { orientation: Qt.Vertical Layout.fillHeight: true Layout.fillWidth: true TreeView { id: treeView Layout.fillHeight: true Layout.fillWidth: true alternatingRowColors: false headerVisible: false selection: sel selectionMode: SelectionMode.SingleSelection itemDelegate: Rectangle { id: assetDelegate // These anchors are important to allow "copy" dragging anchors.verticalCenter: parent ? parent.verticalCenter : undefined anchors.right: parent ? parent.right : undefined property bool isItem : styleData.value !== "root" && styleData.value !== "" property string mimeType : isItem ? assetlist.getMimeType(styleData.value) : "" height: assetText.implicitHeight color: dragArea.containsMouse ? activePalette.highlight : "transparent" Drag.active: isItem ? dragArea.drag.active : false Drag.dragType: Drag.Automatic Drag.supportedActions: Qt.CopyAction Drag.mimeData: isItem ? assetlist.getMimeData(styleData.value) : {} Drag.keys:[ isItem ? assetlist.getMimeType(styleData.value) : "" ] Row { anchors.fill:parent anchors.leftMargin: 1 anchors.topMargin: 1 anchors.bottomMargin: 1 spacing: 4 Image{ id: assetThumb anchors.verticalCenter: parent.verticalCenter visible: assetDelegate.isItem property bool isFavorite: model == undefined || model.favorite === undefined ? false : model.favorite height: parent.height * 0.8 width: height source: 'image://asseticon/' + styleData.value } Label { id: assetText font.bold : assetThumb.isFavorite text: assetlist.getName(styleData.index) } } MouseArea { id: dragArea anchors.fill: assetDelegate hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton drag.target: undefined onReleased: { drag.target = undefined } onPressed: { if (assetDelegate.isItem) { //sel.select(styleData.index, ItemSelectionModel.Select) sel.setCurrentIndex(styleData.index, ItemSelectionModel.ClearAndSelect) if (mouse.button === Qt.LeftButton) { drag.target = parent parent.grabToImage(function(result) { parent.Drag.imageSource = result.url }) } else { drag.target = undefined assetContextMenu.isItemFavorite = assetThumb.isFavorite assetContextMenu.popup() mouse.accepted = false } console.log(parent.Drag.keys) } else { if (treeView.isExpanded(styleData.index)) { treeView.collapse(styleData.index) } else { treeView.expand(styleData.index) } } treeView.focus = true } onDoubleClicked: { if (isItem) { assetlist.activate(styleData.index) } } } } Menu { id: assetContextMenu property bool isItemFavorite property bool isDisplayed: false MenuItem { id: favMenu text: assetContextMenu.isItemFavorite ? "Remove from favorites" : "Add to favorites" property url thumbSource onTriggered: { assetlist.setFavorite(sel.currentIndex, !assetContextMenu.isItemFavorite) } } onAboutToShow: { isDisplayed = true; } onAboutToHide: { isDisplayed = false; } } TableViewColumn { role: "identifier"; title: "Name"; } model: assetListModel Keys.onDownPressed: { sel.setCurrentIndex(assetListModel.getNextChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect) treeView.expand(sel.currentIndex.parent) treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible) } Keys.onUpPressed: { sel.setCurrentIndex(assetListModel.getPreviousChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect) treeView.expand(sel.currentIndex.parent) treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible) } Keys.onReturnPressed: { if (sel.hasSelection) { assetlist.activate(sel.currentIndex) } } } TextArea { id: assetDescription text: "" visible: showDescription.checked readOnly: true Layout.fillWidth: true states: State { name: "hasDescription"; when: assetDescription.text != '' && showDescription.checked PropertyChanges { target: assetDescription; visible: true} } } } } } diff --git a/src/monitor/view/kdenlivemonitorrotoscene.qml b/src/monitor/view/kdenlivemonitorrotoscene.qml index 7df225587..806f13215 100644 --- a/src/monitor/view/kdenlivemonitorrotoscene.qml +++ b/src/monitor/view/kdenlivemonitorrotoscene.qml @@ -1,380 +1,380 @@ import QtQuick 2.6 import QtQuick.Controls 1.4 Item { id: root objectName: "rootrotoscene" SystemPalette { id: activePalette } // default size, but scalable by user height: 300; width: 400 property string comment property string framenum property point profile property point center property real baseUnit: fontMetrics.font.pointSize property double scalex : 1 property double scaley : 1 property double stretch : 1 property double sourcedar : 1 property double offsetx : 0 property double offsety : 0 property double frameSize: 10 property int duration: 300 property double timeScale: 1 property int mouseRulerPos: 0 onOffsetxChanged: canvas.requestPaint() onOffsetyChanged: canvas.requestPaint() onScalexChanged: canvas.requestPaint() onScaleyChanged: canvas.requestPaint() onSourcedarChanged: refreshdar() property bool iskeyframe : true property bool isDefined: false property int requestedKeyFrame : -1 property int requestedSubKeyFrame : -1 property bool requestedCenter : false // The coordinate points where the bezier curve passes property var centerPoints : [] property var centerCross : [] // The control points for the bezier curve points (2 controls points for each coordinate) property var centerPointsTypes : [] property bool showToolbar: false onCenterPointsTypesChanged: checkDefined() signal effectPolygonChanged() signal addKeyframe() signal seekToKeyframe() onDurationChanged: { clipMonitorRuler.updateRuler() } onWidthChanged: { clipMonitorRuler.updateRuler() } onIskeyframeChanged: { console.log('KEYFRAME CHANGED: ', iskeyframe) canvas.requestPaint() } FontMetrics { id: fontMetrics font.family: "Arial" } function refreshdar() { canvas.darOffset = root.sourcedar < root.profile.x * root.stretch / root.profile.y ? (root.profile.x * root.stretch - root.profile.y * root.sourcedar) / (2 * root.profile.x * root.stretch) :(root.profile.y - root.profile.x * root.stretch / root.sourcedar) / (2 * root.profile.y); canvas.requestPaint() } function checkDefined() { root.isDefined = root.centerPointsTypes.length > 0 canvas.requestPaint() } Item { id: monitorOverlay height: root.height - controller.rulerHeight width: root.width Canvas { id: canvas property double handleSize property double darOffset : 0 anchors.fill: parent contextType: "2d"; handleSize: root.baseUnit / 2 renderTarget: Canvas.FramebufferObject renderStrategy: Canvas.Cooperative onPaint: { var ctx = getContext('2d') //if (context) { ctx.clearRect(0,0, width, height); ctx.beginPath() ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.5) ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5) ctx.lineWidth = 2 if (root.centerPoints.length == 0) { // no points defined yet return } var p1 = convertPoint(root.centerPoints[0]) var startP = p1; ctx.moveTo(p1.x, p1.y) if (!isDefined) { ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize); for (var i = 1; i < root.centerPoints.length; i++) { p1 = convertPoint(root.centerPoints[i]) ctx.lineTo(p1.x, p1.y); ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize); } } else { var c1; var c2 var topRight = [] var bottomLeft = [] for (var i = 0; i < root.centerPoints.length; i++) { p1 = convertPoint(root.centerPoints[i]) // Control points var subkf = false if (i == 0) { c1 = convertPoint(root.centerPointsTypes[root.centerPointsTypes.length - 1]) if (root.requestedSubKeyFrame == root.centerPointsTypes.length - 1) { subkf = true } topRight.x = p1.x topRight.y = p1.y bottomLeft.x = p1.x bottomLeft.y = p1.y } else { c1 = convertPoint(root.centerPointsTypes[2*i - 1]) if (root.requestedSubKeyFrame == 2*i - 1) { subkf = true } // Find bounding box topRight.x = Math.max(p1.x, topRight.x) topRight.y = Math.min(p1.y, topRight.y) bottomLeft.x = Math.min(p1.x, bottomLeft.x) bottomLeft.y = Math.max(p1.y, bottomLeft.y) } c2 = convertPoint(root.centerPointsTypes[2*i]) ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, p1.x, p1.y); if (iskeyframe) { if (subkf) { ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8) ctx.fillRect(c1.x - handleSize/2, c1.y - handleSize/2, handleSize, handleSize); ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5) } else { ctx.fillRect(c1.x - handleSize/2, c1.y - handleSize/2, handleSize, handleSize); } if (root.requestedSubKeyFrame == 2 * i) { ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8) ctx.fillRect(c2.x - handleSize/2, c2.y - handleSize/2, handleSize, handleSize); ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5) } else { ctx.fillRect(c2.x - handleSize/2, c2.y - handleSize/2, handleSize, handleSize); } c1 = convertPoint(root.centerPointsTypes[2*i + 1]) ctx.lineTo(c1.x, c1.y); ctx.moveTo(p1.x, p1.y) ctx.lineTo(c2.x, c2.y); ctx.moveTo(p1.x, p1.y) if (i == root.requestedKeyFrame) { ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8) ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize); ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5) } else { ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize); } } } if (root.centerPoints.length > 2) { c1 = convertPoint(root.centerPointsTypes[root.centerPointsTypes.length - 1]) c2 = convertPoint(root.centerPointsTypes[0]) ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, startP.x, startP.y); } centerCross.x = bottomLeft.x + (topRight.x - bottomLeft.x)/2 centerCross.y = topRight.y + (bottomLeft.y - topRight.y)/2 ctx.moveTo(centerCross.x - root.baseUnit, centerCross.y) ctx.lineTo(centerCross.x + root.baseUnit, centerCross.y) ctx.moveTo(centerCross.x, centerCross.y - root.baseUnit) ctx.lineTo(centerCross.x, centerCross.y + root.baseUnit) } ctx.stroke() } function convertPoint(p) { var x = frame.x + p.x * root.scalex var y = frame.y + p.y * root.scaley return Qt.point(x,y); } } Rectangle { id: frame objectName: "referenceframe" property color hoverColor: "#ff0000" width: root.profile.x * root.scalex height: root.profile.y * root.scaley x: root.center.x - width / 2 - root.offsetx; y: root.center.y - height / 2 - root.offsety; color: "transparent" border.color: "#ffffff00" } Rectangle { anchors.centerIn: parent width: label.contentWidth + 6 height: label.contentHeight + 6 visible: !root.isDefined && !global.containsMouse opacity: 0.8 Text { id: label - text: i18n('Click to add points,\nright click to close shape.') + text: i18n("Click to add points,\nright click to close shape.") font.pointSize: root.baseUnit verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter anchors { fill: parent } color: 'black' } color: "yellow" } MouseArea { id: global objectName: "global" acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent property bool pointContainsMouse property bool centerContainsMouse hoverEnabled: true cursorShape: !root.isDefined ? Qt.PointingHandCursor : (pointContainsMouse || centerContainsMouse) ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { if (!root.isDefined) { if (mouse.button == Qt.RightButton && root.centerPoints.length > 2) { // close shape, define control points var p0; var p1; var p2 for (var i = 0; i < root.centerPoints.length; i++) { p1 = root.centerPoints[i] if (i == 0) { p0 = root.centerPoints[root.centerPoints.length - 1] } else { p0 = root.centerPoints[i - 1] } if (i == root.centerPoints.length - 1) { p2 = root.centerPoints[0] } else { p2 = root.centerPoints[i + 1] } var ctrl1 = Qt.point((p0.x - p1.x) / 5, (p0.y - p1.y) / 5); var ctrl2 = Qt.point((p2.x - p1.x) / 5, (p2.y - p1.y) / 5); root.centerPointsTypes.push(Qt.point(p1.x + ctrl1.x, p1.y + ctrl1.y)) root.centerPointsTypes.push(Qt.point(p1.x + ctrl2.x, p1.y + ctrl2.y)) } root.isDefined = true; root.effectPolygonChanged() canvas.requestPaint() } else { var newPoint = Qt.point((mouseX - frame.x) / root.scalex, (mouseY - frame.y) / root.scaley); root.centerPoints.push(newPoint) canvas.requestPaint() } } } onDoubleClicked: { root.addKeyframe() } onPositionChanged: { if (root.iskeyframe == false) return; if (isDefined && pressed) { if (centerContainsMouse) { var xDiff = (mouseX - centerCross.x) / root.scalex var yDiff = (mouseY - centerCross.y) / root.scaley for (var j = 0; j < root.centerPoints.length; j++) { root.centerPoints[j].x += xDiff root.centerPoints[j].y += yDiff root.centerPointsTypes[j * 2].x += xDiff root.centerPointsTypes[j * 2].y += yDiff root.centerPointsTypes[j * 2 + 1].x += xDiff root.centerPointsTypes[j * 2 + 1].y += yDiff } canvas.requestPaint() root.effectPolygonChanged() return } if (root.requestedKeyFrame >= 0) { var xDiff = (mouseX - frame.x) / root.scalex - root.centerPoints[root.requestedKeyFrame].x var yDiff = (mouseY - frame.y) / root.scaley - root.centerPoints[root.requestedKeyFrame].y root.centerPoints[root.requestedKeyFrame].x += xDiff root.centerPoints[root.requestedKeyFrame].y += yDiff root.centerPointsTypes[root.requestedKeyFrame * 2].x += xDiff root.centerPointsTypes[root.requestedKeyFrame * 2].y += yDiff root.centerPointsTypes[root.requestedKeyFrame * 2 + 1].x += xDiff root.centerPointsTypes[root.requestedKeyFrame * 2 + 1].y += yDiff canvas.requestPaint() root.effectPolygonChanged() } else if (root.requestedSubKeyFrame >= 0) { root.centerPointsTypes[root.requestedSubKeyFrame].x = (mouseX - frame.x) / root.scalex root.centerPointsTypes[root.requestedSubKeyFrame].y = (mouseY - frame.y) / root.scaley canvas.requestPaint() root.effectPolygonChanged() } } else if (root.centerPoints.length > 0) { for(var i = 0; i < root.centerPoints.length; i++) { var p1 = canvas.convertPoint(root.centerPoints[i]) if (Math.abs(p1.x - mouseX) <= canvas.handleSize && Math.abs(p1.y - mouseY) <= canvas.handleSize) { if (i == root.requestedKeyFrame) { centerContainsMouse = false pointContainsMouse = true; return; } root.requestedKeyFrame = i canvas.requestPaint() centerContainsMouse = false pointContainsMouse = true; return; } } for(var i = 0; i < root.centerPointsTypes.length; i++) { var p1 = canvas.convertPoint(root.centerPointsTypes[i]) if (Math.abs(p1.x - mouseX) <= canvas.handleSize/2 && Math.abs(p1.y - mouseY) <= canvas.handleSize/2) { if (i == root.requestedSubKeyFrame) { centerContainsMouse = false pointContainsMouse = true; return; } root.requestedSubKeyFrame = i canvas.requestPaint() centerContainsMouse = false pointContainsMouse = true; return; } } if (Math.abs(centerCross.x - mouseX) <= canvas.handleSize/2 && Math.abs(centerCross.y - mouseY) <= canvas.handleSize/2) { centerContainsMouse = true; pointContainsMouse = false; return; } if (root.requestedKeyFrame == -1 && root.requestedSubKeyFrame == -1) { return; } root.requestedKeyFrame = -1 root.requestedSubKeyFrame = -1 pointContainsMouse = false; centerContainsMouse = false canvas.requestPaint() } } } } EffectToolBar { id: effectToolBar anchors { right: parent.right top: parent.top topMargin: 4 rightMargin: 4 } visible: global.mouseX >= x - 10 } MonitorRuler { id: clipMonitorRuler anchors { left: root.left right: root.right bottom: root.bottom } height: controller.rulerHeight } } diff --git a/src/qml/splash.qml b/src/qml/splash.qml index 96b08c388..7b02b4fe1 100644 --- a/src/qml/splash.qml +++ b/src/qml/splash.qml @@ -1,221 +1,221 @@ /*************************************************************************** * 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 . * ***************************************************************************/ import QtQuick 2.6 import QtQuick.Controls 1.5 import QtQuick.Window 2.2 import QtQuick.Layouts 1.3 import QtQuick.Controls.Styles 1.4 Window { id: splash objectName: "splash" color: "transparent" title: "Splash Window" SystemPalette { id: activePalette } modality: Qt.ApplicationModal flags: Qt.SplashScreen property int timeoutInterval: 2000 signal timeout x: (Screen.width - splashContent.width) / 2 y: (Screen.height - splashContent.height) / 2 width: splashContent.width height: splashContent.height property int border: 10 property bool splashing: true function endSplash() { console.log("ending splash") splash.splashing = false; splash.close(); } Rectangle { id:splashContent height: Screen.height / 2 width: Screen.width / 3 border.width:splash.border border.color:"#bfbfbf" color: "#31363b" Image { id:logo anchors.left: splashContent.left anchors.top: splashContent.top anchors.margins: 50 // anchors.horizontalCenter: splashContent.horizontalCenter source: "qrc:/pics/kdenlive-logo.png" fillMode: Image.PreserveAspectFit height: splashContent.height / 5 - 100 } RowLayout { //anchors.horizontalCenter: splashContent.horizontalCenter anchors.bottom: logo.bottom anchors.right: splashContent.right anchors.rightMargin: logo.x spacing: 100 Text { color: "white" - text: i18n('Website') + text: i18n("Website") font.bold: true Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom } Text { color: "white" - text: i18n('Donate') + text: i18n("Donate") font.bold: true Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom } Text { color: "white" - text: i18n('Forum') + text: i18n("Forum") font.bold: true Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom } } Rectangle { id:recentProjects y: splashContent.height / 5 anchors.left: splashContent.left anchors.leftMargin: splash.border anchors.right: splashContent.right anchors.rightMargin: splash.border color:"#232629" height: 3 * splashContent.height / 5 width: splashContent.width visible: !splashing Text { id:txtProject color: "#f38577" - text: i18n('Recent Projects') + text: i18n("Recent Projects") anchors.top: parent.top anchors.left: parent.left anchors.topMargin: 50 anchors.leftMargin: 100 font.bold: true } } Image { id:splash_img y: splashContent.height / 5 anchors.left: splashContent.left anchors.leftMargin: splash.border anchors.right: splashContent.right anchors.rightMargin: splash.border height: 3 * splashContent.height / 5 width: splashContent.width source: "qrc:/pics/splash-background.png" fillMode: Image.PreserveAspectFit visible: splashing } /*Text { id:txtProject color: "#f38577" text: "Recent Projects" anchors.horizontalCenter: splashContent.horizontalCenter anchors.top: logo.bottom anchors.topMargin: 50 font.bold: true } Rectangle { id:recentProjects border.width:5 border.color:"#efefef" color: "#fafafa" height: splashContent.height / 4 width: 5*splashContent.width/6 anchors.horizontalCenter: splashContent.horizontalCenter anchors.top: txtProject.bottom anchors.topMargin: 5 }*/ Item { anchors.left: splashContent.left anchors.right: splashContent.right anchors.bottom: splashContent.bottom anchors.top: recentProjects.bottom anchors.margins: 50 visible: !splashing CheckBox { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter style: CheckBoxStyle { indicator: Rectangle { implicitWidth: 32 implicitHeight: 32 radius: 3 //border.color: control.activeFocus ? "darkblue" : "gray" border.width: 1 border.color:"white" color: "#4d4d4d" Rectangle { visible: control.checked color: "#555" border.color: "#333" radius: 1 anchors.margins: 4 anchors.fill: parent } } label: Text { - text: i18n('Hide on startup') + text: i18n("Hide on startup") color: "white" } } } Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 50 visible: !splashing Button { iconSource:"image://icon/document-new" - text:i18n('New') + text:i18n("New") //style: CustomButton { // backColor: splashContent.color // } } Button { iconSource:"image://icon/document-open" - text:i18n('Open') + text:i18n("Open") //style: CustomButton { // backColor: splashContent.color // } } } } MouseArea { id: clickZone anchors.fill: splashContent onClicked: { console.log("clic"); splash.close(); } } } Component.onCompleted: { visible = true clickZone.focus = true; } } diff --git a/src/timeline2/model/moveableItem.ipp b/src/timeline2/model/moveableItem.ipp index 84294b315..53fe4473c 100644 --- a/src/timeline2/model/moveableItem.ipp +++ b/src/timeline2/model/moveableItem.ipp @@ -1,104 +1,101 @@ /*************************************************************************** * 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 "macros.hpp" #include template MoveableItem::MoveableItem(std::weak_ptr parent, int id) : m_parent(std::move(parent)) , m_id(id == -1 ? TimelineModel::getNextId() : id) , m_position(-1) , m_currentTrackId(-1) , m_grabbed(false) , m_lock(QReadWriteLock::Recursive) { } template int MoveableItem::getId() const { READ_LOCK(); return m_id; } template int MoveableItem::getCurrentTrackId() const { READ_LOCK(); return m_currentTrackId; } template int MoveableItem::getPosition() const { READ_LOCK(); return m_position; } template std::pair MoveableItem::getInOut() const { READ_LOCK(); return {getIn(), getOut()}; } template int MoveableItem::getIn() const { READ_LOCK(); return service()->get_in(); } template int MoveableItem::getOut() const { READ_LOCK(); return service()->get_out(); } template bool MoveableItem::isValid() { READ_LOCK(); return service()->is_valid(); } template void MoveableItem::setPosition(int pos) { QWriteLocker locker(&m_lock); m_position = pos; } template void MoveableItem::setCurrentTrackId(int tid, bool finalMove) { Q_UNUSED(finalMove); QWriteLocker locker(&m_lock); m_currentTrackId = tid; - if (tid == -1) { - selected = false; - } } template void MoveableItem::setInOut(int in, int out) { QWriteLocker locker(&m_lock); service()->set_in_and_out(in, out); } template bool MoveableItem::isGrabbed() const { READ_LOCK(); return m_grabbed; } diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index cef9e84af..6e56d2034 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,1470 +1,1470 @@ /* Copyright (C) 2017 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 "timelinefunctions.hpp" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "logger.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineFunctions") .method("requestClipCut", select_overload, int, int)>(&TimelineFunctions::requestClipCut))( parameter_names("timeline", "clipId", "position")); } bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack, state); return res; } bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (const QString &binId : binIds) { int clipId; if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); bool res = cloneClip(timeline, clipId, newId, state, undo, redo); timeline->m_blockRefresh = true; res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, false, true, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } timeline->m_blockRefresh = false; return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; TRACE_STATIC(timeline, clipId, position); bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } TRACE_RES(result); return result; } bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clipselect = timeline->getGroupElements(clipId); // Remove locked items std::unordered_set clips; for (int cid : clipselect) { int tk = timeline->getClipTrackId(cid); if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) { clips.insert(cid); } } std::unordered_set topElements; std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) timeline->requestClearSelection(); int count = 0; QList newIds; int mainId = -1; QList clipsToCut; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { clipsToCut << cid; } } if (clipsToCut.isEmpty()) { return true; } for (int cid : clipsToCut) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } if (cid == clipId) { mainId = newId; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); newIds << newId; } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hierarchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; bool res = true; for (const int topId : topElements) { res = res && timeline->m_groups->split(topId, criterion, undo, redo); } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1); if (!clips.empty()) { timeline->requestSetSelection(clips); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(itemId); bool isClip = timeline->isClip(itemId); if (isClip) { timeline->requestClipMove(itemId, track, startPosition, false, false); } else { timeline->requestCompositionMove(itemId, track, startPosition, false, false); } std::unordered_set clips = timeline->getGroupElements(itemId); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; //int res = timeline->requestClipsGroup(clips, undo, redo, GroupType::Selection); int res = timeline->m_groups->getRootId(itemId); bool final = false; if (res > -1 || clips.size() == 1) { if (clips.size() > 1) { final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved if (isClip) { final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, undo, redo); } else { final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo); } } } timeline->requestClearSelection(); if (final) { if (startPosition < endPosition) { pCore->pushUndo(undo, redo, i18n("Insert space")); } else { pCore->pushUndo(undo, redo, i18n("Remove space")); } return true; } return false; } bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr &timeline, QVector tracks, QPoint zone, Fun &undo, Fun &redo) { // Check if we have grouped clips that are on unaffected tracks, and ungroup them bool result = true; std::unordered_set affectedItems; // First find all affected items for (int &trackId : tracks) { std::unordered_set items = timeline->getItemsInRange(trackId, zone.x(), zone.y()); affectedItems.insert(items.begin(), items.end()); } for (int item : affectedItems) { if (timeline->m_groups->isInGroup(item)) { int groupId = timeline->m_groups->getRootId(item); std::unordered_set all_children = timeline->m_groups->getLeaves(groupId); for (int child: all_children) { int childTrackId = timeline->getItemTrackId(child); if (!tracks.contains(childTrackId)) { // This item should not be affected by the operation, ungroup it result = result && timeline->requestClipUngroup(child, undo, redo); } } } } return result; } bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = true; result = breakAffectedGroups(timeline, tracks, zone, undo, redo); for (int &trackId : tracks) { if (timeline->getTrackById_const(trackId)->isLocked()) { continue; } result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); } if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo); } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = true; QVector affectedTracks; auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { ++it; continue; } affectedTracks << target_track; ++it; } result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (overwrite) { // Cut all tracks for (int target_track : affectedTracks) { result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (!result) { qDebug() << "// LIFTING ZONE FAILED\n"; break; } } } else { // Cut all tracks for (int target_track : affectedTracks) { int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } } result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } if (result) { if (!trackIds.isEmpty()) { int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, true, undo, redo); } if (result) { pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } } if (!result) { qDebug() << "// REQUESTING SPACE FAILED"; undo(); } return result; } bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId; TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); qDebug() << "/// CUTTING AT START DONE"; } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId; TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); qDebug() << "/// CUTTING AT END DONE"; } } std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips; auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track || timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { std::unordered_set subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true); clips.insert(subs.begin(), subs.end()); } ++it; } bool result = false; if (!clips.empty()) { int clipId = *clips.begin(); if (clips.size() > 1) { int clipsGroup = timeline->m_groups->getRootId(clipId); int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); if (result && res != clipsGroup) { // Only ungroup if a group was created result = timeline->requestClipUngroup(clipId, undo, redo); } if (!result) { undo(); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, true, undo, redo); } } return result; } bool TimelineFunctions::requestInsertSpace(const std::shared_ptr &timeline, QPoint zone, Fun &undo, Fun &redo, bool followTargets) { timeline->requestClearSelection(); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; std::unordered_set items; if (!followTargets) { // Select clips in all tracks items = timeline->getItemsInRange(-1, zone.x(), -1, true); } else { // Select clips in target and active tracks only auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track || timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { std::unordered_set subs = timeline->getItemsInRange(target_track, zone.x(), -1, true); items.insert(subs.begin(), subs.end()); } ++it; } } if (items.empty()) { return true; } timeline->requestSetSelection(items); bool result = true; int itemId = *(items.begin()); int targetTrackId = timeline->getItemTrackId(itemId); int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x(); // TODO the three move functions should be unified in a "requestItemMove" function if (timeline->m_groups->isInGroup(itemId)) { result = result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo); } else if (timeline->isClip(itemId)) { result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, local_undo, local_redo); } else { result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, local_undo, local_redo); } timeline->requestClearSelection(); if (!result) { bool undone = local_undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); res = cloneClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); std::unique_ptr transProps(timeline->m_allCompositions[id]->properties()); res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Successful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId) { PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); PlaylistState::ClipState state = PlaylistState::Disabled; bool disable = true; if (oldState == PlaylistState::Disabled) { state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType(); disable = false; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = changeClipState(timeline, clipId, state, undo, redo); if (result) { pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); } return result; } bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) { int track = timeline->getClipTrackId(clipId); int start = -1; int end = -1; bool invalidate = false; if (track > -1) { if (!timeline->getTrackById_const(track)->isAudioTrack()) { invalidate = true; } start = timeline->getItemPosition(clipId); end = start + timeline->getItemPlaytime(clipId); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // For the state change to work, we need to unplant/replant the clip bool result = true; if (track > -1) { - result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo); + result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false); } result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); if (result && track > -1) { result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups timeline->requestClearSelection(false, undo, redo); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage); return false; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, true, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { timeline->requestSetSelection(clips, undo, redo); pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups timeline->requestClearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); QList possibleTracks = QList() << videoTarget; if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage); return false; } int newId; bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, true, true, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Video")); } return done; } void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = static_cast(compo->getForcedTrack() == -1); bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { timeline->unplantComposition(cid); QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); timeline->replantCompositions(cid, true); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { timeline->unplantComposition(cid); QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); timeline->replantCompositions(cid, true); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable) { QList videoTracks; for (const auto &track : timeline->m_iteratorTable) { if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) { continue; } videoTracks << track.first; } if (videoTracks.size() < 2) { pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage); } // First, dis/enable track compositing QScopedPointer service(timeline->m_tractor->field()); Mlt::Field *field = timeline->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); int added = t.get_int("internal_added"); if (added == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions t.set("disable", enable ? "1" : nullptr); } else if (!enable && added == 200) { field->disconnect_service(t); } } service.reset(service->producer()); } if (enable) { for (int i = 0; i < videoTracks.size(); ++i) { Mlt::Transition transition(*timeline->m_tractor->profile(), "composite"); transition.set("mlt_service", "composite"); transition.set("a_track", 0); transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i))); transition.set("distort", 0); transition.set("aligned", 0); // 200 is an arbitrary number so we can easily remove these transition later transition.set("internal_added", 200); QString geometry; switch (i) { case 0: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("0 0 50% 100%"); break; case 3: geometry = QStringLiteral("0 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 0 33% 50%"); break; default: geometry = QStringLiteral("0 0 33% 33%"); break; } break; case 1: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("50% 0 50% 100%"); break; case 3: geometry = QStringLiteral("33% 0 33% 100%"); break; case 4: geometry = QStringLiteral("50% 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("33% 0 33% 50%"); break; default: geometry = QStringLiteral("33% 0 33% 33%"); break; } break; case 2: switch (videoTracks.size()) { case 3: geometry = QStringLiteral("66% 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("66% 0 33% 50%"); break; default: geometry = QStringLiteral("66% 0 33% 33%"); break; } break; case 3: switch (videoTracks.size()) { case 4: geometry = QStringLiteral("50% 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 50% 33% 50%"); break; default: geometry = QStringLiteral("0 33% 33% 33%"); break; } break; case 4: switch (videoTracks.size()) { case 5: case 6: geometry = QStringLiteral("33% 50% 33% 50%"); break; default: geometry = QStringLiteral("33% 33% 33% 33%"); break; } break; case 5: switch (videoTracks.size()) { case 6: geometry = QStringLiteral("66% 50% 33% 50%"); break; default: geometry = QStringLiteral("66% 33% 33% 33%"); break; } break; case 6: geometry = QStringLiteral("0 66% 33% 33%"); break; case 7: geometry = QStringLiteral("33% 66% 33% 33%"); break; default: geometry = QStringLiteral("66% 66% 33% 33%"); break; } // Add transition to track: transition.set("geometry", geometry.toUtf8().constData()); transition.set("always_active", 1); field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i))); } } field->unlock(); timeline->requestMonitorRefresh(); } void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection, const QDir &targetDir) { bool ok; QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal, QString(), &ok); if (name.isEmpty() || !ok) { return; } if (targetDir.exists(name + QStringLiteral(".mlt"))) { // TODO: warn and ask for overwrite / rename } int offset = -1; int lowerAudioTrack = -1; int lowerVideoTrack = -1; QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt")); // Build a copy of selected tracks. QMap sourceTracks; for (int i : selection) { int sourceTrack = timeline->getItemTrackId(i); int clipPos = timeline->getItemPosition(i); if (offset < 0 || clipPos < offset) { offset = clipPos; } int trackPos = timeline->getTrackMltIndex(sourceTrack); if (!sourceTracks.contains(trackPos)) { sourceTracks.insert(trackPos, sourceTrack); } } // Build target timeline Mlt::Tractor newTractor(*timeline->m_tractor->profile()); QScopedPointer field(newTractor.field()); int ix = 0; QString composite = TransitionsRepository::get()->getCompositingTransition(); QMapIterator i(sourceTracks); QList compositions; while (i.hasNext()) { i.next(); QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile())); newTractor.set_track(*newTrackPlaylist, ix); // QScopedPointer trackProducer(newTractor.track(ix)); int trackId = i.value(); sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if (isAudio) { newTrackPlaylist->set("hide", 1); if (lowerAudioTrack < 0) { lowerAudioTrack = ix; } } else { newTrackPlaylist->set("hide", 2); if (lowerVideoTrack < 0) { lowerVideoTrack = ix; } } for (int itemId : selection) { if (timeline->getItemTrackId(itemId) == trackId) { // Copy clip on the destination track if (timeline->isClip(itemId)) { int clip_position = timeline->m_allClips[itemId]->getPosition(); auto clip_loc = track->getClipIndexAt(clip_position); int target_clip = clip_loc.second; QSharedPointer clip = track->getClipProducer(target_clip); newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1); } else if (timeline->isComposition(itemId)) { // Composition auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get()); QString id(t->get("kdenlive_id")); QString internal(t->get("internal_added")); if (internal.isEmpty()) { compositions << t; if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service"); t->set("kdenlive_id", t->get("mlt_service")); } } } } } ix++; } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) { Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service")); Mlt::Properties sourceProps(t->get_properties()); newComposition.inherit(sourceProps); QString id(t->get("kdenlive_id")); int in = qMax(0, t->get_in() - offset); int out = t->get_out() - offset; newComposition.set_in_and_out(in, out); int a_track = sourceTracks.value(t->get_a_track()); int b_track = sourceTracks.value(t->get_b_track()); field->plant_transition(newComposition, a_track, b_track); } } } // Track compositing i.toFront(); ix = 0; while (i.hasNext()) { i.next(); int trackId = i.value(); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) { // add track compositing / mix Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData()); if (isAudio) { t.set("sum", 1); } t.set("always_active", 1); t.set("internal_added", 237); field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix); } ix++; } Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.connect(newTractor); xmlConsumer.run(); } int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack) { qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack; int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); int destTrackMltIndex = timeline->getTrackMltIndex(destTrack); int offset = 0; qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex; if (masterTrackMltIndex == destTrackMltIndex) { return offset; } int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1; bool isAudio = timeline->isAudioTrack(startTrack); int track = masterTrackMltIndex; while (track != destTrackMltIndex) { track += step; qDebug() << "+ + +TESTING TRACK: " << track; int trackId = timeline->getTrackIndexFromPosition(track - 1); if (isAudio == timeline->isAudioTrack(trackId)) { offset += step; } } return offset; } int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset) { int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); bool isAudio = timeline->isAudioTrack(startTrack); if (isAudio != audioOffset) { offset = -offset; } qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset; while (offset != 0) { masterTrackMltIndex += offset > 0 ? 1 : -1; qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex; if (masterTrackMltIndex < 0) { masterTrackMltIndex = 0; break; } if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) { masterTrackMltIndex = (int)timeline->m_allTracks.size(); break; } int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); if (timeline->isAudioTrack(trackId) == isAudio) { offset += offset > 0 ? -1 : 1; } } return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); } QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline) { QList audioTracks; QList videoTracks; for (const auto &track : timeline->m_allTracks) { if (track->isAudioTrack()) { audioTracks << track->getId(); } else { videoTracks << track->getId(); } } return {audioTracks, videoTracks}; } QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set &itemIds) { int clipId = *(itemIds.begin()); // We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips std::unordered_set allIds; for (const auto &itemId : itemIds) { std::unordered_set siblings = timeline->getGroupElements(itemId); allIds.insert(siblings.begin(), siblings.end()); } timeline->requestClearSelection(); // TODO better guess for master track int masterTid = timeline->getItemTrackId(clipId); bool audioCopy = timeline->isAudioTrack(masterTid); int masterTrack = timeline->getTrackPosition(masterTid); QDomDocument copiedItems; int offset = -1; QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); copiedItems.appendChild(container); QStringList binIds; for (int id : allIds) { if (offset == -1 || timeline->getItemPosition(id) < offset) { offset = timeline->getItemPosition(id); } if (timeline->isClip(id)) { container.appendChild(timeline->m_allClips[id]->toXml(copiedItems)); const QString bid = timeline->m_allClips[id]->binId(); if (!binIds.contains(bid)) { binIds << bid; } } else if (timeline->isComposition(id)) { container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems)); } else { Q_ASSERT(false); } } QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); container.appendChild(container2); for (const QString &id : binIds) { std::shared_ptr clip = pCore->projectItemModel()->getClipByBinID(id); QDomDocument tmp; container2.appendChild(clip->toXml(tmp)); } container.setAttribute(QStringLiteral("offset"), offset); if (audioCopy) { container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack); int masterMirror = timeline->getMirrorVideoTrackId(masterTid); if (masterMirror == -1) { QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); if (!projectTracks.second.isEmpty()) { masterTrack = timeline->getTrackPosition(projectTracks.second.first()); } } else { masterTrack = timeline->getTrackPosition(masterMirror); } } /* masterTrack contains the reference track over which we want to paste. this is a video track, unless audioCopy is defined */ container.setAttribute(QStringLiteral("masterTrack"), masterTrack); container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); container.appendChild(grp); std::unordered_set groupRoots; std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); qDebug() << "==============\n GROUP ROOTS: "; for (int gp : groupRoots) { qDebug() << "GROUP: " << gp; } qDebug() << "\n======="; grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots))); qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; return copiedItems.toString(); } bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position) { timeline->requestClearSelection(); QDomDocument copiedItems; copiedItems.setContent(pasteString); if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { qDebug() << " / / READING CLIPS FROM CLIPBOARD"; } else { return false; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); QMap mappedIds; // Check available tracks QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt(); QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); // find paste tracks // List of all source audio tracks QList audioTracks; // List of all source video tracks QList videoTracks; // List of all audio tracks with their corresponding video mirror std::unordered_map audioMirrors; // List of all source audio tracks that don't have video mirror QList singleAudioTracks; for (int i = 0; i < clips.count(); i++) { QDomElement prod = clips.at(i).toElement(); int trackPos = prod.attribute(QStringLiteral("track")).toInt(); bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack")); if (audioTrack) { if (!audioTracks.contains(trackPos)) { audioTracks << trackPos; } int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt(); if (videoMirror == -1) { if (singleAudioTracks.contains(trackPos)) { continue; } singleAudioTracks << trackPos; continue; } audioMirrors[trackPos] = videoMirror; if (videoTracks.contains(videoMirror)) { continue; } videoTracks << videoMirror; } else { if (videoTracks.contains(trackPos)) { continue; } videoTracks << trackPos; } } for (int i = 0; i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); int trackPos = prod.attribute(QStringLiteral("track")).toInt(); if (!videoTracks.contains(trackPos)) { videoTracks << trackPos; } int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt(); if (atrackPos == 0 || videoTracks.contains(atrackPos)) { continue; } videoTracks << atrackPos; } // Now we have a list of all source tracks, check that we have enough target tracks std::sort(videoTracks.begin(), videoTracks.end()); std::sort(audioTracks.begin(), audioTracks.end()); std::sort(singleAudioTracks.begin(), singleAudioTracks.end()); //qDebug()<<"== GOT WANTED TKS\n VIDEO: "< projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); return false; } // Find destination master track // Check we have enough tracks above/below if (requestedVideoTracks > 0) { qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks; int tracksBelow = masterSourceTrack - videoTracks.first(); int tracksAbove = videoTracks.last() - masterSourceTrack; qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove; qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId; qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId); if (projectTracks.second.indexOf(trackId) < tracksBelow) { qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; // not enough tracks below, try to paste on upper track trackId = projectTracks.second.at(tracksBelow); } else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) { // not enough tracks above, try to paste on lower track qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove); trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1); } } else { // Audio only masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt(); int tracksBelow = masterSourceTrack - audioTracks.first(); int tracksAbove = audioTracks.last() - masterSourceTrack; if (projectTracks.first.indexOf(trackId) < tracksBelow) { qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; // not enough tracks below, try to paste on upper track trackId = projectTracks.first.at(tracksBelow); } else if ((projectTracks.first.size() - (projectTracks.first.indexOf(trackId) + 1)) < tracksAbove) { // not enough tracks above, try to paste on lower track qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.first.size() - tracksAbove); trackId = projectTracks.first.at(projectTracks.first.size() - tracksAbove - 1); } } QMap tracksMap; bool audioMaster = false; int masterIx = projectTracks.second.indexOf(trackId); if (masterIx == -1) { masterIx = projectTracks.first.indexOf(trackId); audioMaster = true; } qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId; qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack; qDebug() << "/// MASTER PASTE: " << masterIx; for (int tk : videoTracks) { int newPos = masterIx + tk - masterSourceTrack; if (newPos < 0 || newPos >= projectTracks.second.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); } tracksMap.insert(tk, projectTracks.second.at(newPos)); } bool audioOffsetCalculated = false; int audioOffset = 0; for (const auto &mirror : audioMirrors) { int videoIx = tracksMap.value(mirror.second); tracksMap.insert(mirror.first, timeline->getMirrorAudioTrackId(videoIx)); if (!audioOffsetCalculated) { int oldPosition = mirror.first; int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition)); audioOffset = currentPosition - oldPosition; audioOffsetCalculated = true; } } if (!audioOffsetCalculated && audioMaster) { audioOffset = masterIx - masterSourceTrack; audioOffsetCalculated = true; } for (int i = 0; i < singleAudioTracks.size(); i++) { int oldPos = singleAudioTracks.at(i); if (tracksMap.contains(oldPos)) { continue; } int offsetId = oldPos + audioOffset; if (offsetId < 0 || offsetId >= projectTracks.first.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); return false; } tracksMap.insert(oldPos, projectTracks.first.at(offsetId)); } if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { // paste from another document, import bin clips QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); if (folderId.isEmpty()) { // Folder doe not exist const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); } QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < binClips.count(); ++i) { QDomElement currentProd = binClips.item(i).toElement(); QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); if (!pCore->projectItemModel()->isIdFree(clipId)) { QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); mappedIds.insert(clipId, updatedId); clipId = updatedId; } pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo); } } int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); bool res = true; QLocale locale; std::unordered_map correspondingIds; QList waitingIds; for (int i = 0; i < clips.count(); i++) { waitingIds << i; } for (int i = 0; res && !waitingIds.isEmpty();) { if (i >= waitingIds.size()) { i = 0; } QDomElement prod = clips.at(waitingIds.at(i)).toElement(); QString originalId = prod.attribute(QStringLiteral("binid")); if (mappedIds.contains(originalId)) { // Map id originalId = mappedIds.value(originalId); } int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; double speed = locale.toDouble(prod.attribute(QStringLiteral("speed"))); int newId; bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, undo, redo); if (created) { // Master producer is ready // ids.removeAll(originalId); waitingIds.removeAt(i); } else { i++; qApp->processEvents(); continue; } if (timeline->m_allClips[newId]->m_endlessResize) { out = out - in; in = 0; timeline->m_allClips[newId]->m_producer->set("length", out + 1); } timeline->m_allClips[newId]->setInOut(in, out); int targetId = prod.attribute(QStringLiteral("id")).toInt(); correspondingIds[targetId] = newId; res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo); // paste effects if (res) { std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo); } } // Compositions for (int i = 0; res && i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("composition")); int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt(); if (aTrackId > 0) { aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId)); } int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; int newId; auto transProps = std::make_unique(); QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < props.count(); j++) { transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), props.at(j).toElement().text().toUtf8().constData()); } res = timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in + 1, std::move(transProps), newId, undo, redo); } if (!res) { undo(); return false; } // Rebuild groups const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); if (!groupsData.isEmpty()) { timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo); } // unsure to clear selection in undo/redo too. Fun unselect = [&]() { qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection; timeline->requestClearSelection(); qDebug() << "after Selection " << timeline->m_currentSelection; return true; }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); pCore->pushUndo(undo, redo, i18n("Paste clips")); return true; } bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr &timeline, int trackId, int position, bool affectAllTracks) { // find blank duration int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(position); int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position); if (cid == -1) { return false; } int start = timeline->getItemPosition(cid); requestSpacerEndOperation(timeline, cid, start, start - spaceDuration); return true; } diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index 5989d4527..54352222a 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,3358 +1,3358 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock")) .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))( parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))( parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))( parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) .method("requestItemResize", select_overload(&TimelineModel::requestItemResize))( parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( parameter_names("itemIds", "logUndo", "type")) .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo")) .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))( parameter_names("pos", "id", "trackName", "audioTrack")) .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")) .method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion")) .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear")) .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId")) .method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds")) .method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance")) .method("suggestCompositionMove", &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance")) // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("requestCompositionInsertion", select_overload, int &, bool)>( // &TimelineModel::requestCompositionInsertion))( // parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo")) .method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed")); } int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_tractor(new Mlt::Tractor(*profile)) , m_snaps(new SnapModel()) , m_undoStack(std::move(undo_stack)) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_blockRefresh(false) , m_closing(false) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); m_blackClip->set("set.test_audio", 0); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } void TimelineModel::prepareClose() { requestClearSelection(true); QWriteLocker locker(&m_lock); // Unlock all tracks to allow delting clip from tracks m_closing = true; auto it = m_allTracks.begin(); while (it != m_allTracks.end()) { (*it)->unlock(); ++it; } } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.cbegin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isItem(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.cbegin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.cend(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return (*it)->getId(); } } return 0; } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { return getTrackMltIndex((*it)->getId()); } } return 0; } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; if (it != m_allTracks.cend()) { ++it; } while (it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } ++it; } if (it != m_allTracks.cend() && !(*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; while (it != m_allTracks.cbegin()) { --it; if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; // qDebug()<<"MOVING CLIP FROM: "< 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); TRACE_RES(res); return res; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, false, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = sourceTrackId; } if (currentPos == position && sourceTrackId == trackId) { TRACE_RES(position); return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { TRACE_RES(position); return position; } if (sourceTrackId == -1) { // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away... TRACE_RES(currentPos); return currentPos; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId && sourceTrackId != -1) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { TRACE_RES(position); return position; } } int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { TRACE_RES(currentPos); return currentPos; } possible = requestClipMove(clipId, trackId, position, true, false, false); TRACE_RES(possible ? position : currentPos); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check space after the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1; if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); possible = requestClipMove(clipId, trackId, updatedPos, true, false, false); if (possible) { TRACE_RES(updatedPos); return updatedPos; } } TRACE_RES(currentPos); return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(compoId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (getTrackById_const(trackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = currentTrack; } if (currentPos == position && currentTrack == trackId) { TRACE_RES(position); return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { TRACE_RES(position); return position; } /*bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position;*/ TRACE_RES(currentPos); return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug() << " / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state; return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { QWriteLocker locker(&m_lock); TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } else if (useTargets && getTrackById_const(trackId)->isLocked()) { // Video target set but locked trackId = m_audioTarget; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } if (trackId == -1) { pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); return false; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, logUndo, local_undo, local_redo); int target_track; if (audioDrop) { target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget; } else { target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget; } qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); int mirror = getMirrorTrackId(trackId); if (mirror > -1 && getTrackById_const(mirror)->isLocked()) { mirror = -1; } bool canMirrorDrop = !useTargets && mirror > -1; if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = mirror; } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, true, true, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, undo, redo); } if (isClip(itemId)) { return requestClipDeletion(itemId, undo, redo); } if (isComposition(itemId)) { return requestCompositionDeletion(itemId, undo, redo); } Q_ASSERT(false); return false; } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isItem(itemId)); QString actionLabel; if (m_groups->isInGroup(itemId)) { actionLabel = i18n("Remove group"); } else { if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); } else { actionLabel = i18n("Delete Composition"); } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestItemDeletion(itemId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } TRACE_RES(res); requestClearSelection(true); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { - bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); + bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo, false, true); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { - bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo); + bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { if (track->isLocked()) { continue; } std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); Q_ASSERT(isItem(itemId)); if (getGroupElements(groupId).count(itemId) == 0) { // this group doesn't contain the clip, abort return false; } bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; std::unordered_set all_clips; std::unordered_set all_compositions; // Separate clips from compositions to sort for (int affectedItemId : all_items) { if (isClip(affectedItemId)) { all_clips.insert(affectedItemId); } else { all_compositions.insert(affectedItemId); } } // Sort clips first std::vector sorted_clips(all_clips.begin(), all_clips.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) { int p1 = m_allClips[clipId1]->getPosition(); int p2 = m_allClips[clipId2]->getPosition(); return delta_pos > 0 ? p2 <= p1 : p1 <= p2; }); // Sort compositions. We need to delete in the move direction from top to bottom std::vector sorted_compositions(all_compositions.begin(), all_compositions.end()); std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) { int p1 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2; }); sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end()); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = [this, finalMove]() { if (finalMove) { updateDuration(); } return true; }; // Check if there is a track move bool updatePositionOnly = false; // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, finalMove, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } if (finalMove) { updateDuration(); } return true; }; } std::unordered_map old_track_ids, old_position, old_forced_track; // First, remove clips - if (delta_track != 0) { // We delete our clips only if changing track for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { - ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, true); + ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, true, false); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } } // We need to insert depending on the move direction to avoid confusing the view // std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); bool updateThisView = allowViewRefresh; if (delta_track == 0) { // Special case, we are moving on same track, avoid too many calculations for (int item : sorted_clips) { int current_track_id = getItemTrackId(item); int target_position = getItemPosition(item) + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, current_track_id, target_position, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } else { ok = ok && requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } else { // Track changed for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, finalMove, local_undo, local_redo, true); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, logUndo); if (!m_groups->isInGroup(clipId)) { TRACE_RES(false); return false; } bool res = requestItemDeletion(clipId, logUndo); TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); bool isSelection = m_currentSelection == current_group; if (isSelection) { m_currentSelection = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { if (m_groups->getType(current_group) == GroupType::Selection) { Q_ASSERT(isSelection); // in the case of a selection group, we delete the group but don't log it in the undo object Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; m_groups->ungroupItem(one_child, tmp_undo, tmp_redo); } else { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } const QVariantList TimelineModel::getGroupData(int itemId) { QWriteLocker locker(&m_lock); if (!m_groups->isInGroup(itemId)) { return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)}; } int groupId = m_groups->getRootId(itemId); QVariantList result; std::unordered_set items = m_groups->getLeaves(groupId); for (int id : items) { result << id << getItemPosition(id) << getItemPlaytime(id); } return result; } void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right) { Q_ASSERT(startPos.size() == endPos.size()); QMap> startData; QMap> endData; while (!startPos.isEmpty()) { int id = startPos.takeFirst().toInt(); int in = startPos.takeFirst().toInt(); int duration = startPos.takeFirst().toInt(); startData.insert(id, {in, duration}); id = endPos.takeFirst().toInt(); in = endPos.takeFirst().toInt(); duration = endPos.takeFirst().toInt(); endData.insert(id, {in, duration}); } QMapIterator> i(startData); QList changedItems; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; while (i.hasNext()) { i.next(); QPair startItemPos = i.value(); QPair endItemPos = endData.value(i.key()); if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) { // Revert individual items to original position requestItemResize(i.key(), startItemPos.second, right, false, 0, true); changedItems << i.key(); } } for (int id : changedItems) { QPair endItemPos = endData.value(id); result = result & requestItemResize(id, endItemPos.second, right, true, undo, redo, false); if (!result) { break; } } if (result) { PUSH_UNDO(undo, redo, i18n("Resize group")); } else { undo(); } } const std::vector TimelineModel::getBoundaries(int itemId) { std::vector boundaries; std::unordered_set items; if (m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); items = m_groups->getLeaves(groupId); } else { items.insert(itemId); } for (int id : items) { if (isClip(id) || isComposition(id)) { int in = getItemPosition(id); int out = in + getItemPlaytime(id); boundaries.push_back(in); boundaries.push_back(out); } } return boundaries; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isItem(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0 && getItemTrackId(itemId) != -1) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; if (right && size > out - in && isClip(itemId)) { int targetPos = in + size - 1; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; } } else if (!right && size > (out - in) && isClip(itemId)) { int targetPos = out - size; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = out - getTrackById_const(trackId)->getBlankStart(in - 1); } } int timelinePos = pCore->getTimelinePosition(); m_snaps->addPoint(timelinePos); int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); m_snaps->removePoint(timelinePos); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } all_items.insert(itemId); for (int id : items) { if (id == itemId) { continue; } int start = getItemPosition(id); int end = start + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; int finalPos = right ? in + size : out - size; int finalSize; int resizedCount = 0; for (int id : all_items) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } if (right) { finalSize = finalPos - getItemPosition(id); } else { finalSize = getItemPosition(id) + getItemPlaytime(id) - finalPos; } result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo); resizedCount++; } if (!result || resizedCount == 0) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } int res = result ? size : -1; TRACE_RES(res); return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isItem(itemId)); if (getItemTrackId(itemId) != -1) { qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++"; QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); TRACE(ids, logUndo, type); if (type == GroupType::Selection || type == GroupType::Leaf) { // Selections shouldn't be done here. Call requestSetSelection instead TRACE_RES(-1); return -1; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; int result = requestClipsGroup(ids, undo, redo, type); if (result > -1 && logUndo) { PUSH_UNDO(undo, redo, i18n("Group clips")); } TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { QWriteLocker locker(&m_lock); if (type != GroupType::Selection) { requestClearSelection(); } int clipsCount = 0; QList tracks; for (int id : ids) { if (isClip(id)) { int trackId = getClipTrackId(id); if (trackId == -1) { return -1; } tracks << trackId; clipsCount++; } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } if (type == GroupType::Selection && ids.size() == 1) { // only one element selected, no group created return -1; } if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) { // Check if we are grouping an AVSplit std::unordered_set::const_iterator it = ids.begin(); int firstId = *it; std::advance(it, 1); int secondId = *it; bool isAVGroup = false; if (getClipBinId(firstId) == getClipBinId(secondId)) { if (getClipState(firstId) == PlaylistState::AudioOnly) { if (getClipState(secondId) == PlaylistState::VideoOnly) { isAVGroup = true; } } else if (getClipState(secondId) == PlaylistState::AudioOnly) { isAVGroup = true; } } if (isAVGroup) { type = GroupType::AVSplit; } } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type != GroupType::Selection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return groupId; } bool TimelineModel::requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemIds, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; requestClearSelection(); std::unordered_set roots; std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); for (int root : roots) { if (isGroup(root)) { result = result && requestClipUngroup(root, undo, redo); } } if (!result) { bool undone = undo(); Q_ASSERT(undone); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); requestClearSelection(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; result = requestClipUngroup(itemId, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection; if (!isSelection) { requestClearSelection(); } bool res = m_groups->ungroupItem(itemId, undo, redo); if (res && !isSelection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return res; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo, true); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); if (updateView) { _resetView(); } auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, true); if (updateView) { _resetView(); } return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track QWriteLocker locker(&m_lock); TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); _resetView(); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert) { // qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { // qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); requestClearSelection(true); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isItem(int id) const { return isClip(id) || isComposition(id); } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { if (m_closing) { return; } int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set("out", duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::getBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::getNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::getPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { TRACE(pos); return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { TRACE(pos); return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo) { QWriteLocker locker(&m_lock); // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } // TRACE_RES(result); return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, std::move(transProps)); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point requestClearSelection(true); clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); notifyChange(modelIndex, modelIndex, StartRole); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); - ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo); + ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo, false); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of a_track, and increasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.emplace_back(trackPos, compo.first); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [&](const std::pair &a, const std::pair &b) { if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) { return a.first < b.first; } return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack(); }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count()); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); notifyChange(modelIndex, modelIndex, ItemATrack); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } if (!clip->checkConsistency()) { qDebug() << "Consistency check failed for clip" << cp.first; return false; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { auto tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } // Check that the selection is in a valid state: if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isGroup(m_currentSelection)) { qDebug() << "Selection is in inconsistent state"; return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } std::shared_ptr TimelineModel::producer() { return std::make_shared(tractor()); } void TimelineModel::checkRefresh(int start, int end) { if (m_blockRefresh) { return; } int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { - getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo); + getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo, false, false); } if (old_trackId != -1) { m_allClips[clipId]->refreshProducerFromBin(); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { - success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); + success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { QWriteLocker locker(&m_lock); TRACE(clipId, speed); Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); } if (!result) { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); TRACE_RES(false); return false; } } else { // If clip is not inserted on a track, we just change the producer result = m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); } TRACE_RES(result); return result; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.cbegin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.cend()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.cbegin()) { --it; if ((*it)->isAudioTrack() == audioWanted) { return (*it)->getId(); } } return trackId; } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.cend()) { ++it; if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.cend() ? trackId : (*it)->getId(); } bool TimelineModel::requestClearSelection(bool onDeletion) { QWriteLocker locker(&m_lock); TRACE(); if (m_currentSelection == -1) { TRACE_RES(true); return true; } if (isGroup(m_currentSelection)) { // Reset offset display on clips - std::unordered_set items = getCurrentSelection(); + std::unordered_set items = m_groups->getLeaves(m_currentSelection); for (auto &id : items) { if (isGroup(id)) { std::unordered_set children = m_groups->getLeaves(id); for (int c : children) { items.insert(c); } } else if (isClip(id)) { m_allClips[id]->clearOffset(); m_allClips[id]->setGrab(false); m_allClips[id]->setSelected(false); } else if (isComposition(id)) { m_allCompositions[id]->setGrab(false); m_allCompositions[id]->setSelected(false); } if (m_groups->getType(m_currentSelection) == GroupType::Selection) { m_groups->destructGroupItem(m_currentSelection); } } } else { if (isClip(m_currentSelection)) { m_allClips[m_currentSelection]->setGrab(false); m_allClips[m_currentSelection]->setSelected(false); } else if (isComposition(m_currentSelection)) { m_allCompositions[m_currentSelection]->setGrab(false); m_allCompositions[m_currentSelection]->setSelected(false); } Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection)); } m_currentSelection = -1; emit selectionChanged(); TRACE_RES(true); return true; } void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo) { Fun operation = [this, onDeletion]() { requestClearSelection(onDeletion); return true; }; Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } std::unordered_set TimelineModel::getCurrentSelection() const { READ_LOCK(); if (m_currentSelection == -1) { return {}; } if (isGroup(m_currentSelection)) { return m_groups->getLeaves(m_currentSelection); } else { Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection)); return {m_currentSelection}; } } + void TimelineModel::requestAddToSelection(int itemId, bool clear) { QWriteLocker locker(&m_lock); TRACE(itemId, clear); if (clear) { requestClearSelection(); } std::unordered_set selection = getCurrentSelection(); if (selection.count(itemId) == 0) { selection.insert(itemId); requestSetSelection(selection); } } void TimelineModel::requestRemoveFromSelection(int itemId) { QWriteLocker locker(&m_lock); TRACE(itemId); std::unordered_set all_items = {itemId}; int parentGroup = m_groups->getDirectAncestor(itemId); if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) { all_items = m_groups->getLeaves(parentGroup); } std::unordered_set selection = getCurrentSelection(); for (int current_itemId : all_items) { if (selection.count(current_itemId) > 0) { selection.erase(current_itemId); } } requestSetSelection(selection); } bool TimelineModel::requestSetSelection(const std::unordered_set &ids) { QWriteLocker locker(&m_lock); TRACE(ids); requestClearSelection(); // if the items are in groups, we must retrieve their topmost containing groups std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); bool result = true; if (roots.size() == 0) { m_currentSelection = -1; } else if (roots.size() == 1) { m_currentSelection = *(roots.begin()); setSelected(m_currentSelection, true); } else { Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (ids.size() == 2) { // Check if we selected 2 clips from the same master QList pairIds; for (auto &id : roots) { if (isClip(id)) { pairIds << id; } } if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) { // Check if they have same bin id // Both clips have same bin ID, display offset int pos1 = getClipPosition(pairIds.at(0)); int pos2 = getClipPosition(pairIds.at(1)); if (pos2 > pos1) { int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0))); if (offset != 0) { m_allClips[pairIds.at(1)]->setOffset(offset); m_allClips[pairIds.at(0)]->setOffset(-offset); } } else { int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1))); if (offset != 0) { m_allClips[pairIds.at(0)]->setOffset(offset); m_allClips[pairIds.at(1)]->setOffset(-offset); } } } } result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0; Q_ASSERT(m_currentSelection >= 0); } emit selectionChanged(); return result; } void TimelineModel::setSelected(int itemId, bool sel) { if (isClip(itemId)) { m_allClips[itemId]->setSelected(sel); } else if (isComposition(itemId)) { m_allCompositions[itemId]->setSelected(sel); } else if (isGroup(itemId)) { auto leaves = m_groups->getLeaves(itemId); for (auto &id : leaves) { setSelected(id, true); } } } bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Fun reverse = [this]() { requestClearSelection(false); return true; }; Fun operation = [this, ids]() { return requestSetSelection(ids); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } void TimelineModel::setTrackLockedState(int trackId, bool lock) { QWriteLocker locker(&m_lock); TRACE(trackId, lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; Fun lock_lambda = [this, trackId]() { getTrackById(trackId)->lock(); return true; }; Fun unlock_lambda = [this, trackId]() { getTrackById(trackId)->unlock(); return true; }; if (lock) { if (lock_lambda()) { UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Lock track")); } } else { if (unlock_lambda()) { UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo); PUSH_UNDO(undo, redo, i18n("Unlock track")); } } } std::unordered_set TimelineModel::getAllTracksIds() const { READ_LOCK(); std::unordered_set result; std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; }); return result; } diff --git a/src/timeline2/model/trackmodel.cpp b/src/timeline2/model/trackmodel.cpp index 7aea94377..7babc9054 100644 --- a/src/timeline2/model/trackmodel.cpp +++ b/src/timeline2/model/trackmodel.cpp @@ -1,1261 +1,1267 @@ /*************************************************************************** * 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 "trackmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinemodel.hpp" #include #include #include TrackModel::TrackModel(const std::weak_ptr &parent, int id, const QString &trackName, bool audioTrack) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) , m_lock(QReadWriteLock::Recursive) { if (auto ptr = parent.lock()) { m_track = std::make_shared(*ptr->getProfile()); m_playlists[0].set_profile(*ptr->getProfile()); m_playlists[1].set_profile(*ptr->getProfile()); m_track->insert_track(m_playlists[0], 0); m_track->insert_track(m_playlists[1], 1); if (!trackName.isEmpty()) { m_track->set("kdenlive:track_name", trackName.toUtf8().constData()); } if (audioTrack) { m_track->set("kdenlive:audio_track", 1); for (auto &m_playlist : m_playlists) { m_playlist.set("hide", 1); } } m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight()); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) { if (auto ptr2 = m_parent.lock()) { QModelIndex ix = ptr2->makeTrackIndexFromID(m_id); ptr2->dataChanged(ix, ix, roles); } }); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) { if (auto ptr = parent.lock()) { m_track = std::make_shared(mltTrack); m_playlists[0] = *m_track->track(0); m_playlists[1] = *m_track->track(1); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::~TrackModel() { m_track->remove_track(1); m_track->remove_track(0); } int TrackModel::construct(const std::weak_ptr &parent, int id, int pos, const QString &trackName, bool audioTrack) { std::shared_ptr track(new TrackModel(parent, id, trackName, audioTrack)); TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack); id = track->m_id; if (auto ptr = parent.lock()) { ptr->registerTrack(std::move(track), pos); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } return id; } int TrackModel::getClipsCount() { READ_LOCK(); #ifdef QT_DEBUG int count = 0; for (auto &m_playlist : m_playlists) { for (int i = 0; i < m_playlist.count(); i++) { if (!m_playlist.is_blank(i)) { count++; } } } Q_ASSERT(count == static_cast(m_allClips.size())); #else int count = (int)m_allClips.size(); #endif return count; } Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove) { QWriteLocker locker(&m_lock); // By default, insertion occurs in topmost track // Find out the clip id at position int target_clip = m_playlists[0].get_clip_index_at(position); int count = m_playlists[0].count(); if (auto ptr = m_parent.lock()) { Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1); } else { qDebug() << "impossible to get parent timeline"; Q_ASSERT(false); } // we create the function that has to be executed after the melt order. This is essentially book-keeping auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) { if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_allClips[clip->getId()] = clip; // store clip // update clip position and track clip->setPosition(position); clip->setSubPlaylistIndex(subPlaylist); int new_in = clip->getPosition(); int new_out = new_in + clip->getPlaytime(); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (updateView) { int clip_index = getRowfromClip(clipId); ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index); ptr->_endInsertRows(); bool audioOnly = clip->isAudioOnly(); if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(new_in, new_out); } if (!audioOnly && finalMove && !isAudioTrack()) { ptr->invalidateZone(new_in, new_out); } } return true; } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; if (target_clip >= count && isBlankAt(position)) { // In that case, we append after, in the first playlist return [this, position, clipId, end_function, finalMove, groupMove]() { if (isLocked()) return false; if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); clip->setCurrentTrackId(m_id, finalMove); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); if (finalMove && !groupMove) { ptr->updateDuration(); } return index != -1 && end_function(0); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } if (isBlankAt(position)) { int blank_end = getBlankEnd(position); int length = -1; if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); length = clip->getPlaytime(); } if (blank_end >= position + length) { return [this, position, clipId, end_function]() { if (isLocked()) return false; if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); clip->setCurrentTrackId(m_id); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); return index != -1 && end_function(0); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } } return []() { return false; }; } bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } if (position < 0) { return false; } if (auto ptr = m_parent.lock()) { if (isAudioTrack() && !ptr->getClipPtr(clipId)->canBeAudio()) { qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK"; return false; } if (!isAudioTrack() && !ptr->getClipPtr(clipId)->canBeVideo()) { qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK"; return false; } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool res = true; if (ptr->getClipPtr(clipId)->clipState() != PlaylistState::Disabled) { res = res && ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); } auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove); res = res && operation(); if (res) { auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove); UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool undone = local_undo(); Q_ASSERT(undone); return false; } return false; } void TrackModel::replugClip(int clipId) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); std::unique_ptr prod(m_playlists[target_track].replace_with_blank(target_clip)); if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_playlists[target_track].insert_at(clip_position, *clip, 1); if (!clip->isAudioOnly() && !isAudioTrack()) { ptr->invalidateZone(clip->getIn(), clip->getOut()); } if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(clip->getIn(), clip->getOut()); } } m_playlists[target_track].consolidate_blanks(); m_playlists[target_track].unlock(); } Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allClips[clipId]->getPosition(); bool audioOnly = m_allClips[clipId]->isAudioOnly(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, groupMove, this]() { if (isLocked()) return false; auto clip_loc = getClipIndexAt(clip_position); if (updateView) { int old_clip_index = getRowfromClip(clipId); auto ptr = m_parent.lock(); ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); ptr->_endRemoveRows(); } int target_track = m_allClips[clipId]->getSubPlaylistIndex(); int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); auto prod = m_playlists[target_track].replace_with_blank(target_clip); if (prod != nullptr) { m_playlists[target_track].consolidate_blanks(); m_allClips[clipId]->setCurrentTrackId(-1); m_allClips[clipId]->setSubPlaylistIndex(-1); m_allClips.erase(clipId); delete prod; m_playlists[target_track].unlock(); if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); if (finalMove) { if (!audioOnly && !isAudioTrack()) { ptr->invalidateZone(old_in, old_out); } if (!groupMove && target_clip >= m_playlists[target_track].count()) { // deleted last clip in playlist ptr->updateDuration(); } } if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(old_in, old_out); } } return true; } m_playlists[target_track].unlock(); return false; }; } -bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove) +bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (isLocked()) { return false; } auto old_clip = m_allClips[clipId]; int old_position = old_clip->getPosition(); // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView; + if (finalDeletion) { + m_allClips[clipId]->selected = false; + } auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove); if (operation()) { auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } int TrackModel::getBlankSizeAtPos(int frame) { READ_LOCK(); int min_length = 0; for (auto &m_playlist : m_playlists) { int ix = m_playlist.get_clip_index_at(frame); if (m_playlist.is_blank(ix)) { int blank_length = m_playlist.clip_length(ix); if (min_length == 0 || (blank_length > 0 && blank_length < min_length)) { min_length = blank_length; } } } return min_length; } int TrackModel::suggestCompositionLength(int position) { READ_LOCK(); if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) { return -1; } auto clip_loc = getClipIndexAt(position); int track = clip_loc.first; int index = clip_loc.second; int other_index; // index in the other track int other_track = (track + 1) % 2; int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); other_index = m_playlists[other_track].get_clip_index_at(end_pos); if (other_index < m_playlists[other_track].count()) { end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index)); } int min = -1; std::unordered_set existing = getCompositionsInRange(position, end_pos); if (existing.size() > 0) { for (int id : existing) { if (min < 0) { min = m_allCompositions[id]->getPosition(); } else { min = qMin(min, m_allCompositions[id]->getPosition()); } } } if (min >= 0) { // An existing composition is limiting the space end_pos = min; } return end_pos - position; } int TrackModel::getBlankSizeNearClip(int clipId, bool after) { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int track = clip_loc.first; int index = clip_loc.second; int other_index; // index in the other track int other_track = (track + 1) % 2; if (after) { int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); other_index = m_playlists[other_track].get_clip_index_at(first_pos); index++; } else { int last_pos = m_playlists[track].clip_start(index) - 1; other_index = m_playlists[other_track].get_clip_index_at(last_pos); index--; } if (index < 0) return 0; int length = INT_MAX; if (index < m_playlists[track].count()) { if (!m_playlists[track].is_blank(index)) { return 0; } length = std::min(length, m_playlists[track].clip_length(index)); } if (other_index < m_playlists[other_track].count()) { if (!m_playlists[other_track].is_blank(other_index)) { return 0; } length = std::min(length, m_playlists[other_track].clip_length(other_index)); } return length; } int TrackModel::getBlankSizeNearComposition(int compoId, bool after) { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); int clip_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(clip_position) > 0); Q_ASSERT(m_compoPos[clip_position] == compoId); auto it = m_compoPos.find(clip_position); int clip_length = m_allCompositions[compoId]->getPlaytime(); int length = INT_MAX; if (after) { ++it; if (it != m_compoPos.end()) { return it->first - clip_position - clip_length; } } else { if (it != m_compoPos.begin()) { --it; return clip_position - it->first - m_allCompositions[it->second]->getPlaytime(); } return clip_position; } return length; } Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; Q_ASSERT(target_clip < m_playlists[target_track].count()); int size = out - in + 1; bool checkRefresh = false; if (!isHidden() && !isAudioTrack()) { checkRefresh = true; } auto update_snaps = [old_in, old_out, checkRefresh, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (checkRefresh) { ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); // ptr->adjustAssetRange(clipId, m_allClips[clipId]->getIn(), m_allClips[clipId]->getOut()); } } else { qDebug() << "Error : clip resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; int delta = m_allClips[clipId]->getPlaytime() - size; if (delta == 0) { return []() { return true; }; } // qDebug() << "RESIZING CLIP: " << clipId << " FROM: " << delta; if (delta > 0) { // we shrink clip return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() { if (isLocked()) return false; int target_clip_mutable = target_clip; int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable; // insert blank to space that is going to be empty // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1 m_playlists[target_track].insert_blank(blank_index, delta - 1); if (!right) { m_allClips[clipId]->setPosition(clip_position + delta); // Because we inserted blank before, the index of our clip has increased target_clip_mutable++; } int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); // make sure to do this after, to avoid messing the indexes m_playlists[target_track].consolidate_blanks(); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } } return err == 0; }; } int blank = -1; int other_blank_end = getBlankEnd(clip_position, (target_track + 1) % 2); if (right) { if (target_clip == m_playlists[target_track].count() - 1 && other_blank_end >= out) { // clip is last, it can always be extended return [this, target_clip, target_track, in, out, update_snaps, clipId]() { if (isLocked()) return false; // color, image and title clips can have unlimited resize QScopedPointer clip(m_playlists[target_track].get_clip(target_clip)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } int err = m_playlists[target_track].resize_clip(target_clip, in, out); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); if (m_playlists[target_track].count() - 1 == target_clip) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } return err == 0; }; } blank = target_clip + 1; } else { if (target_clip == 0) { // clip is first, it can never be extended on the left return []() { return false; }; } blank = target_clip - 1; } if (m_playlists[target_track].is_blank(blank)) { int blank_length = m_playlists[target_track].clip_length(blank); if (blank_length + delta >= 0 && other_blank_end >= out) { return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() { if (isLocked()) return false; int target_clip_mutable = target_clip; int err = 0; if (blank_length + delta == 0) { err = m_playlists[target_track].remove(blank); if (!right) { target_clip_mutable--; } } else { err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1); } if (err == 0) { QScopedPointer clip(m_playlists[target_track].get_clip(target_clip_mutable)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); } if (!right && err == 0) { m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable)); } if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); return err == 0; }; } } return []() { return false; }; } int TrackModel::getId() const { return m_id; } int TrackModel::getClipByPosition(int position) { READ_LOCK(); QSharedPointer prod(nullptr); if (m_playlists[0].count() > 0) { prod = QSharedPointer(m_playlists[0].get_clip_at(position)); } if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { prod = QSharedPointer(m_playlists[1].get_clip_at(position)); } if (!prod || prod->is_blank()) { return -1; } return prod->get_int("_kdenlive_cid"); } QSharedPointer TrackModel::getClipProducer(int clipId) { READ_LOCK(); QSharedPointer prod(nullptr); if (m_playlists[0].count() > 0) { prod = QSharedPointer(m_playlists[0].get_clip(clipId)); } if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { prod = QSharedPointer(m_playlists[1].get_clip(clipId)); } return prod; } int TrackModel::getCompositionByPosition(int position) { READ_LOCK(); for (const auto &comp : m_compoPos) { if (comp.first == position) { return comp.second; } else if (comp.first < position) { if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) { return comp.second; } } } return -1; } int TrackModel::getClipByRow(int row) const { READ_LOCK(); if (row >= static_cast(m_allClips.size())) { return -1; } auto it = m_allClips.cbegin(); std::advance(it, row); return (*it).first; } std::unordered_set TrackModel::getClipsInRange(int position, int end) { READ_LOCK(); std::unordered_set ids; for (const auto &clp : m_allClips) { int pos = clp.second->getPosition(); int length = clp.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(clp.first); } } return ids; } int TrackModel::getRowfromClip(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return (int)std::distance(m_allClips.begin(), m_allClips.find(clipId)); } std::unordered_set TrackModel::getCompositionsInRange(int position, int end) { READ_LOCK(); // TODO: this function doesn't take into accounts the fact that there are two tracks std::unordered_set ids; for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); int length = compo.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(compo.first); } } return ids; } int TrackModel::getRowfromComposition(int tid) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(tid) > 0); return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(tid)); } QVariant TrackModel::getProperty(const QString &name) const { READ_LOCK(); return QVariant(m_track->get(name.toUtf8().constData())); } void TrackModel::setProperty(const QString &name, const QString &value) { QWriteLocker locker(&m_lock); m_track->set(name.toUtf8().constData(), value.toUtf8().constData()); // Hide property mus be defined at playlist level or it won't be saved if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) { for (auto &m_playlist : m_playlists) { m_playlist.set(name.toUtf8().constData(), value.toInt()); } } } bool TrackModel::checkConsistency() { auto ptr = m_parent.lock(); if (!ptr) { return false; } auto check_blank_zone = [&](int playlist, int in, int out) { if (in >= m_playlists[playlist].get_playtime()) { return true; } int index = m_playlists[playlist].get_clip_index_at(in); if (!m_playlists[playlist].is_blank(index)) { return false; } int cin = m_playlists[playlist].clip_start(index); if (cin > in) { return false; } if (cin + m_playlists[playlist].clip_length(index) - 1 < out) { return false; } return true; }; std::vector> clips; // clips stored by (position, id) for (const auto &c : m_allClips) { Q_ASSERT(c.second); Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get()); clips.emplace_back(c.second->getPosition(), c.first); } std::sort(clips.begin(), clips.end()); int last_out = 0; for (size_t i = 0; i < clips.size(); ++i) { auto cur_clip = m_allClips[clips[i].second]; if (last_out < clips[i].first) { // we have some blank space before this clip, check it for (int pl = 0; pl <= 1; ++pl) { if (!check_blank_zone(pl, last_out, clips[i].first - 1)) { qDebug() << "ERROR: Some blank was required on playlist " << pl << " between " << last_out << " and " << clips[i].first - 1; return false; } } } int cur_playlist = cur_clip->getSubPlaylistIndex(); int clip_index = m_playlists[cur_playlist].get_clip_index_at(clips[i].first); if (m_playlists[cur_playlist].is_blank(clip_index)) { qDebug() << "ERROR: Found blank when clip was required at position " << clips[i].first; return false; } if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) { qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first; return false; } if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) { qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first; return false; } if (m_playlists[cur_playlist].clip_length(clip_index) != cur_clip->getPlaytime()) { qDebug() << "ERROR: Inconsistent length for clip at position " << clips[i].first; return false; } auto pr = m_playlists[cur_playlist].get_clip(clip_index); Mlt::Producer prod(pr); if (!prod.same_clip(*cur_clip)) { qDebug() << "ERROR: Wrong clip at position " << clips[i].first; delete pr; return false; } delete pr; // the current playlist is valid, we check that the other is essentially blank int other_playlist = (cur_playlist + 1) % 2; int in_blank = clips[i].first; int out_blank = clips[i].first + cur_clip->getPlaytime() - 1; // the previous clip on the same playlist must not intersect int prev_clip_id_same_playlist = -1; for (int j = (int)i - 1; j >= 0; --j) { if (cur_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) { prev_clip_id_same_playlist = j; break; } } if (prev_clip_id_same_playlist >= 0 && clips[(size_t)prev_clip_id_same_playlist].first + m_allClips[clips[(size_t)prev_clip_id_same_playlist].second]->getPlaytime() > clips[i].first) { qDebug() << "ERROR: found overlapping clips at position " << clips[i].first; return false; } // the previous clip on the other playlist might restrict the blank in/out int prev_clip_id_other_playlist = -1; for (int j = (int)i - 1; j >= 0; --j) { if (other_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) { prev_clip_id_other_playlist = j; break; } } if (prev_clip_id_other_playlist >= 0) { in_blank = std::max(in_blank, clips[(size_t)prev_clip_id_other_playlist].first + m_allClips[clips[(size_t)prev_clip_id_other_playlist].second]->getPlaytime()); } // the next clip on the other playlist might restrict the blank in/out int next_clip_id_other_playlist = -1; for (int j = (int)i + 1; j < (int)clips.size(); ++j) { if (other_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) { next_clip_id_other_playlist = j; break; } } if (next_clip_id_other_playlist >= 0) { out_blank = std::min(out_blank, clips[(size_t)next_clip_id_other_playlist].first - 1); } if (in_blank <= out_blank && !check_blank_zone(other_playlist, in_blank, out_blank)) { qDebug() << "ERROR: we expected blank on playlist " << other_playlist << " between " << in_blank << " and " << out_blank; return false; } last_out = clips[i].first + cur_clip->getPlaytime(); } int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime()); if (!clips.empty() && playtime != clips.back().first + m_allClips[clips.back().second]->getPlaytime()) { qDebug() << "Error: playtime is " << playtime << " but was expected to be" << clips.back().first + m_allClips[clips.back().second]->getPlaytime(); return false; } // We now check compositions positions if (m_allCompositions.size() != m_compoPos.size()) { qDebug() << "Error: the number of compositions position doesn't match number of compositions"; return false; } for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); if (m_compoPos.count(pos) == 0) { qDebug() << "Error: the position of composition " << compo.first << " is not properly stored"; return false; } if (m_compoPos[pos] != compo.first) { qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos; return false; } } for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) { int compoId = it->second; int cur_in = m_allCompositions[compoId]->getPosition(); Q_ASSERT(cur_in == it->first); int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1; ++it; if (it != m_compoPos.end()) { int next_compoId = it->second; int next_in = m_allCompositions[next_compoId]->getPosition(); int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1; if (next_in <= cur_out) { qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ " << next_in << ", " << next_out << "]"; return false; } } --it; } return true; } std::pair TrackModel::getClipIndexAt(int position) { READ_LOCK(); for (int j = 0; j < 2; j++) { if (!m_playlists[j].is_blank_at(position)) { return {j, m_playlists[j].get_clip_index_at(position)}; } } Q_ASSERT(false); return {-1, -1}; } bool TrackModel::isBlankAt(int position) { READ_LOCK(); return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position); } int TrackModel::getBlankStart(int position) { READ_LOCK(); int result = 0; for (auto &m_playlist : m_playlists) { if (m_playlist.count() == 0) { break; } if (!m_playlist.is_blank_at(position)) { result = position; break; } int clip_index = m_playlist.get_clip_index_at(position); int start = m_playlist.clip_start(clip_index); if (start > result) { result = start; } } return result; } int TrackModel::getBlankEnd(int position, int track) { READ_LOCK(); // Q_ASSERT(m_playlists[track].is_blank_at(position)); if (!m_playlists[track].is_blank_at(position)) { return position; } int clip_index = m_playlists[track].get_clip_index_at(position); int count = m_playlists[track].count(); if (clip_index < count) { int blank_start = m_playlists[track].clip_start(clip_index); int blank_length = m_playlists[track].clip_length(clip_index); return blank_start + blank_length; } return INT_MAX; } int TrackModel::getBlankEnd(int position) { READ_LOCK(); int end = INT_MAX; for (int j = 0; j < 2; j++) { end = std::min(getBlankEnd(position, j), end); } return end; } Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out, bool logUndo) { QWriteLocker locker(&m_lock); int compo_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(compo_position) > 0); Q_ASSERT(m_compoPos[compo_position] == compoId); int old_in = compo_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime() - 1; qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out; if (out == -1) { out = in + old_out - old_in; } auto update_snaps = [old_in, old_out, logUndo, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out + 1); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); if (logUndo) { ptr->invalidateZone(old_in, old_out); ptr->invalidateZone(new_in, new_out); } // ptr->adjustAssetRange(compoId, new_in, new_out); } else { qDebug() << "Error : Composition resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; if (in == compo_position && (out == -1 || out == old_out)) { return []() { qDebug() << "//// NO MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; return true; }; } // temporary remove of current compo to check collisions qDebug() << "// CURRENT COMPOSITIONS ----\n" << m_compoPos << "\n--------------"; m_compoPos.erase(compo_position); bool intersecting = hasIntersectingComposition(in, out); // put it back m_compoPos[compo_position] = compoId; if (intersecting) { return []() { qDebug() << "//// FALSE MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; return false; }; } return [in, out, compoId, update_snaps, this]() { if (isLocked()) return false; m_compoPos.erase(m_allCompositions[compoId]->getPosition()); m_allCompositions[compoId]->setInOut(in, out); update_snaps(in, out + 1); m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId; return true; }; } bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } auto operation = requestCompositionInsertion_lambda(compoId, position, updateView, finalMove); if (operation()) { auto reverse = requestCompositionDeletion_lambda(compoId, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } -bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo) +bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool finalDeletion) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } Q_ASSERT(m_allCompositions.count(compoId) > 0); auto old_composition = m_allCompositions[compoId]; int old_position = old_composition->getPosition(); Q_ASSERT(m_compoPos.count(old_position) > 0); Q_ASSERT(m_compoPos[old_position] == compoId); + if (finalDeletion) { + m_allCompositions[compoId]->selected = false; + } auto operation = requestCompositionDeletion_lambda(compoId, updateView, finalMove); if (operation()) { auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allCompositions[compoId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime(); return [compoId, old_in, old_out, updateView, finalMove, this]() { if (isLocked()) return false; int old_clip_index = getRowfromComposition(compoId); auto ptr = m_parent.lock(); if (updateView) { ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); ptr->_endRemoveRows(); } m_allCompositions[compoId]->setCurrentTrackId(-1); m_allCompositions.erase(compoId); m_compoPos.erase(old_in); ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); if (finalMove) { ptr->invalidateZone(old_in, old_out); } return true; }; } int TrackModel::getCompositionByRow(int row) const { READ_LOCK(); if (row < (int)m_allClips.size()) { return -1; } Q_ASSERT(row <= (int)m_allClips.size() + (int)m_allCompositions.size()); auto it = m_allCompositions.cbegin(); std::advance(it, row - (int)m_allClips.size()); return (*it).first; } int TrackModel::getCompositionsCount() const { READ_LOCK(); return (int)m_allCompositions.size(); } Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); bool intersecting = true; if (auto ptr = m_parent.lock()) { intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1); } else { qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; } if (!intersecting) { return [compoId, this, position, updateView, finalMove]() { if (isLocked()) return false; if (auto ptr = m_parent.lock()) { std::shared_ptr composition = ptr->getCompositionPtr(compoId); m_allCompositions[composition->getId()] = composition; // store clip // update clip position and track composition->setCurrentTrackId(getId()); int new_in = position; int new_out = new_in + composition->getPlaytime(); composition->setInOut(new_in, new_out - 1); if (updateView) { int composition_index = getRowfromComposition(composition->getId()); ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index); ptr->_endInsertRows(); } ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); m_compoPos[new_in] = composition->getId(); if (finalMove) { ptr->invalidateZone(new_in, new_out); } return true; } qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; return false; }; } return []() { return false; }; } bool TrackModel::hasIntersectingComposition(int in, int out) const { READ_LOCK(); auto it = m_compoPos.lower_bound(in); if (m_compoPos.empty()) { return false; } if (it != m_compoPos.end() && it->first <= out) { // compo at it intersects return true; } if (it == m_compoPos.begin()) { return false; } --it; int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1; return end >= in; return false; } bool TrackModel::addEffect(const QString &effectId) { READ_LOCK(); return m_effectStack->appendEffect(effectId); } const QString TrackModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } bool TrackModel::stackEnabled() const { READ_LOCK(); return m_effectStack->isStackEnabled(); } void TrackModel::setEffectStackEnabled(bool enable) { m_effectStack->setEffectStackEnabled(enable); } int TrackModel::trackDuration() { return m_track->get_length(); } bool TrackModel::isLocked() const { READ_LOCK(); return m_track->get_int("kdenlive:locked_track"); } bool TrackModel::isTimelineActive() const { READ_LOCK(); return m_track->get_int("kdenlive:timeline_active"); } bool TrackModel::shouldReceiveTimelineOp() const { READ_LOCK(); return isTimelineActive() && !isLocked(); } bool TrackModel::isAudioTrack() const { return m_track->get_int("kdenlive:audio_track") == 1; } PlaylistState::ClipState TrackModel::trackType() const { return (m_track->get_int("kdenlive:audio_track") == 1 ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); } bool TrackModel::isHidden() const { return m_track->get_int("hide") & 1; } bool TrackModel::isMute() const { return m_track->get_int("hide") & 2; } bool TrackModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(std::move(service), trackType()); return true; } bool TrackModel::copyEffect(const std::shared_ptr &stackModel, int rowId) { QWriteLocker locker(&m_lock); return m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); } void TrackModel::lock() { setProperty(QStringLiteral("kdenlive:locked_track"), QStringLiteral("1")); if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeTrackIndexFromID(m_id); ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole}); } } void TrackModel::unlock() { setProperty(QStringLiteral("kdenlive:locked_track"), (char *)nullptr); if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeTrackIndexFromID(m_id); ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole}); } } diff --git a/src/timeline2/model/trackmodel.hpp b/src/timeline2/model/trackmodel.hpp index 0d5c0554f..7f4a3315f 100644 --- a/src/timeline2/model/trackmodel.hpp +++ b/src/timeline2/model/trackmodel.hpp @@ -1,288 +1,290 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TRACKMODEL_H #define TRACKMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include #include #include class TimelineModel; class ClipModel; class CompositionModel; class EffectStackModel; /* @brief This class represents a Track object, as viewed by the backend. To allow same track transitions, a Track object corresponds to two Mlt::Playlist, between which we can switch when required by the transitions. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class TrackModel { public: TrackModel() = delete; ~TrackModel(); friend class ClipModel; friend class CompositionModel; friend class TimelineController; friend struct TimelineFunctions; friend class TimelineItemModel; friend class TimelineModel; private: /* This constructor is private, call the static construct instead */ TrackModel(const std::weak_ptr &parent, int id = -1, const QString &trackName = QString(), bool audioTrack = false); TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id = -1); public: /* @brief Creates a track, which references itself to the parent Returns the (unique) id of the created track @param id Requested id of the track. Automatic if id = -1 @param pos is the optional position of the track. If left to -1, it will be added at the end */ static int construct(const std::weak_ptr &parent, int id = -1, int pos = -1, const QString &trackName = QString(), bool audioTrack = false); /* @brief returns the number of clips */ int getClipsCount(); /* @brief returns the number of compositions */ int getCompositionsCount() const; /* Perform a split at the requested position */ bool splitClip(QSharedPointer caller, int position); /* Implicit conversion operator to access the underlying producer */ operator Mlt::Producer &() { return *m_track.get(); } /* @brief Returns true if track is in locked state */ bool isLocked() const; /* @brief Returns true if track is active in timeline, ie. * will receive insert/lift/overwrite/extract operations */ bool isTimelineActive() const; /* @brief Returns true if track is active and not locked */ bool shouldReceiveTimelineOp() const; /* @brief Returns true if track is an audio track */ bool isAudioTrack() const; /* @brief Returns the track type (audio / video) */ PlaylistState::ClipState trackType() const; /* @brief Returns true if track is disabled */ bool isHidden() const; /* @brief Returns true if track is disabled */ bool isMute() const; // TODO make protected QVariant getProperty(const QString &name) const; void setProperty(const QString &name, const QString &value); protected: /* @brief This will lock the track: it will no longer allow insertion/deletion/resize of items This functions are dangerous to call directly since locking the track will potentially mess up with the undo/redo system (a track lock may make an undo impossible). Prefer calling TimelineModel::setTrackLockedState */ void lock(); void unlock(); /* @brief Returns a lambda that performs a resize of the given clip. The lamda returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param in is the new starting on the clip @param out is the new ending on the clip @param right is true if we change the right side of the clip, false otherwise */ Fun requestClipResize_lambda(int clipId, int in, int out, bool right); /* @brief Performs an insertion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clip is the id of the clip @param position is the position where to insert the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove = false); /* @brief This function returns a lambda that performs the requested operation */ Fun requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove = false); /* @brief Performs an deletion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation + @param groupMove If true, this is part of a larger operation and some operations like checking track duration will not be performed and have to be performed separately + @param finalDeletion If true, the clip will be deselected (should be false if this is a clip move doing delte/insert) */ - bool requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove = false); + bool requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion); /* @brief This function returns a lambda that performs the requested operation */ - Fun requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove = false); + Fun requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove); /* @brief Performs an insertion of the given composition. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. Note that in Mlt, the composition insertion logic is not really at the track level, but we use that level to do collision checking @param compoId is the id of the composition @param position is the position where to insert the composition @param updateView whether we send update to the view @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief This function returns a lambda that performs the requested operation */ Fun requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove = false); - bool requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo); + bool requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool finalDeletion); Fun requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove = false); Fun requestCompositionResize_lambda(int compoId, int in, int out = -1, bool logUndo = false); /* @brief Returns the size of the blank before or after the given clip @param clipId is the id of the clip @param after is true if we query the blank after, false otherwise */ int getBlankSizeNearClip(int clipId, bool after); int getBlankSizeNearComposition(int compoId, bool after); int getBlankStart(int position); int getBlankSizeAtPos(int frame); /*@brief Returns the best composition duration depending on clips on the track */ int suggestCompositionLength(int position); /*@brief Returns the (unique) construction id of the track*/ int getId() const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding clip id. If it does not exist, returns -1 */ int getClipByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding composition id. If it does not exist, returns -1 */ int getCompositionByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a clip ID, returns the row of the clip. */ int getRowfromClip(int clipId) const; /*@brief This function is used only by the QAbstractItemModel Given a composition ID, returns the row of the composition. */ int getRowfromComposition(int compoId) const; /*@brief This is an helper function that test frame level consistency with the MLT structures */ bool checkConsistency(); /* @brief Returns true if we have a composition intersecting with the range [in,out]*/ bool hasIntersectingComposition(int in, int out) const; /* @brief This is an helper function that returns the sub-playlist in which the clip is inserted, along with its index in the playlist @param position the position of the target clip*/ std::pair getClipIndexAt(int position); QSharedPointer getClipProducer(int clipId); /* @brief This is an helper function that checks in all playlists if the given position is a blank */ bool isBlankAt(int position); /* @brief This is an helper function that returns the end of the blank that covers given position */ int getBlankEnd(int position); /* Same, but we restrict to a specific track*/ int getBlankEnd(int position, int track); /* @brief Returns the clip id on this track at position requested, or -1 if no clip */ int getClipByPosition(int position); /* @brief Returns the composition id on this track starting position requested, or -1 if not found */ int getCompositionByPosition(int position); /* @brief Add a track effect */ bool addEffect(const QString &effectId); /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; /* @brief Returns true if effect stack is enabled */ bool stackEnabled() const; /* @brief Enable / disable the track's effect stack */ void setEffectStackEnabled(bool enable); /* @brief This function removes the clip from the mlt object, and then insert it back in the same spot again. * This is used when some properties of the clip have changed, and we need this to refresh it */ void replugClip(int clipId); int trackDuration(); /* @brief Returns the list of the ids of the clips that intersect the given range */ std::unordered_set getClipsInRange(int position, int end = -1); /* @brief Returns the list of the ids of the compositions that intersect the given range */ std::unordered_set getCompositionsInRange(int position, int end); /* @brief Import effects from a service that contains some (another track) */ bool importEffects(std::weak_ptr service); /* @brief Copy effects from anoter effect stack */ bool copyEffect(const std::shared_ptr &stackModel, int rowId); public slots: /*Delete the current track and all its associated clips */ void slotDelete(); private: std::weak_ptr m_parent; int m_id; // this is the creation id of the track, used for book-keeping // We fake two playlists to allow same track transitions. std::shared_ptr m_track; Mlt::Playlist m_playlists[2]; std::map> m_allClips; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map> m_allCompositions; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map m_compoPos; // We store the positions of the compositions. In Melt, the compositions are not inserted at the track level, but we keep // those positions here to check for moves and resize mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access protected: std::shared_ptr m_effectStack; }; #endif diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml index fa57ee4bb..dd7df95be 100644 --- a/src/timeline2/view/qml/Clip.qml +++ b/src/timeline2/view/qml/Clip.qml @@ -1,940 +1,940 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.6 import QtQuick.Controls 2.2 import Kdenlive.Controls 1.0 import QtQml.Models 2.2 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 string hash: 'ccc' //TODO property double speed: 1.0 property color borderColor: 'black' property bool forceReloadThumb property bool isComposition: false property var groupTrimData width : clipDuration * timeScale; opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0 signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim) signal trimmedIn(var clip, bool shiftTrim) signal initGroupTrim(var clip) signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim) signal trimmedOut(var clip, bool shiftTrim) onIsGrabbedChanged: { if (clipRoot.isGrabbed) { 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) } onInPointChanged: { if (parentTrack && parentTrack.isAudio && thumbsLoader.item) { thumbsLoader.item.reload() } } onClipResourceChanged: { if (itemType == ProducerType.Color) { color: Qt.darker(getColor(), 1.5) } } ToolTip { visible: mouseArea.containsMouse && !dragProxyArea.pressed font.pixelSize: root.baseUnit delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text text: label.text + ' (' + timeline.timecode(clipRoot.inPoint) + '-' + timeline.timecode(clipRoot.outPoint) + ')' } } onKeyframeModelChanged: { console.log('keyframe model changed............') if (effectRow.keyframecanvas) { effectRow.keyframecanvas.requestPaint() } } onClipDurationChanged: { width = clipDuration * timeScale; } 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 if (parentTrack && parentTrack.isAudio && thumbsLoader.item) { thumbsLoader.item.reload(); } } 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 + '/' + (isImage ? '#0' : '#') property string inThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(inPoint * speed) property string outThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(outPoint * speed) 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: dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { root.stopScrolling = true 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) 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); } 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: parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : "" onLoaded: { item.reload() } } } 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.pixelSize: root.baseUnit * 1.2 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 font.pixelSize: root.baseUnit delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text - text: i18n('Offset') + (positionOffset < 0 ? ( ': -' + timeline.timecode(-positionOffset)) : ': ' + timeline.timecode(positionOffset)) + text: i18n("Offset") + (positionOffset < 0 ? ( ': -' + timeline.timecode(-positionOffset)) : ': ' + timeline.timecode(positionOffset)) } } Text { id: offsetLabel text: positionOffset font.pixelSize: root.baseUnit * 1.2 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.pixelSize: root.baseUnit * 1.2 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: timeline.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers && parent.width > width * 1.5 text: model.comment font.pixelSize: root.baseUnit 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.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 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) endDrag() } onReleased: { parent.opacity = 0 } ToolTip { visible: compInArea.containsMouse && !dragProxyArea.pressed font.pixelSize: root.baseUnit delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text - text: i18n('Click to add composition') + 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 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) endDrag() } onReleased: { parent.opacity = 0 } ToolTip { visible: compOutArea.containsMouse && !dragProxyArea.pressed font.pixelSize: root.baseUnit delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text - text: i18n('Click to add composition') + 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 } Rectangle { id: fadeInControl anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined 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.stopScrolling = true startX = Math.round(parent.x / timeScale) startFadeIn = clipRoot.fadeIn parent.anchors.left = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 fadeInTriangle.opacity = 0.5 // parentTrack.clipSelected(clipRoot, parentTrack) TODO } onReleased: { root.stopScrolling = false fadeInTriangle.opacity = 0.3 parent.opacity = 0 if (fadeInTriangle.width > parent.radius) parent.anchors.horizontalCenter = fadeInTriangle.right else parent.anchors.left = fadeInTriangle.left 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) if (duration != clipRoot.fadeIn) { timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1) // Show fade duration as time in a "bubble" help. var s = timeline.timecode(Math.max(duration, 0)) bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s) } } } } 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 } } } 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} } Rectangle { id: fadeOutControl anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined anchors.top: fadeOutCanvas.top anchors.topMargin: -10 width: root.baseUnit * 2 height: width radius: width / 2 color: '#66FFFFFF' border.width: 2 border.color: 'red' 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.stopScrolling = true startX = Math.round(parent.x / timeScale) startFadeOut = clipRoot.fadeOut parent.anchors.right = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 fadeOutCanvas.opacity = 0.5 } onReleased: { fadeOutCanvas.opacity = 0.3 parent.opacity = 0 root.stopScrolling = false if (fadeOutCanvas.width > parent.radius) parent.anchors.horizontalCenter = fadeOutCanvas.left else parent.anchors.right = fadeOutCanvas.right 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.timecode(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 } } } } Rectangle { id: trimIn anchors.left: clipRoot.left anchors.leftMargin: 0 height: parent.height enabled: !isLocked width: 5 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) MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false property bool shiftTrim: false property bool sizeChanged: false cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); onPressed: { root.stopScrolling = true clipRoot.originalX = clipRoot.x clipRoot.originalDuration = clipDuration parent.anchors.left = undefined shiftTrim = mouse.modifiers & Qt.ShiftModifier if (!shiftTrim && clipRoot.grouped) { clipRoot.initGroupTrim(clipRoot) } parent.opacity = 0 } onReleased: { root.stopScrolling = false parent.anchors.left = clipRoot.left if (sizeChanged) { clipRoot.trimmedIn(clipRoot, shiftTrim) sizeChanged = false } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((trimIn.x) / timeScale) if (delta !== 0) { if (delta < -modelStart) { delta = -modelStart } var newDuration = clipDuration - delta sizeChanged = true clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim) } } } onEntered: { if (!pressed) { parent.opacity = 0.5 } } onExited: { parent.opacity = 0 } } } Rectangle { id: trimOut anchors.right: clipRoot.right anchors.rightMargin: 0 height: parent.height width: 5 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) MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true property bool shiftTrim: false property bool sizeChanged: false cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false onPressed: { root.stopScrolling = true clipRoot.originalDuration = clipDuration parent.anchors.right = undefined shiftTrim = mouse.modifiers & Qt.ShiftModifier if (!shiftTrim && clipRoot.grouped) { clipRoot.initGroupTrim(clipRoot) } parent.opacity = 0 } onReleased: { root.stopScrolling = false parent.anchors.right = clipRoot.right if (sizeChanged) { clipRoot.trimmedOut(clipRoot, shiftTrim) sizeChanged = false } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var newDuration = Math.round((parent.x + parent.width) / timeScale) if (newDuration != clipDuration) { sizeChanged = true clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim) } } } onEntered: { if (!pressed) { parent.opacity = 0.5 } } onExited: parent.opacity = 0 } } /*MenuItem { id: mergeItem - text: i18n('Merge with next clip') + text: i18n("Merge with next clip") onTriggered: timeline.mergeClipWithNext(trackIndex, index, false) } MenuItem { - text: i18n('Rebuild Audio Waveform') + 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/ClipMenu.qml b/src/timeline2/view/qml/ClipMenu.qml index b621e2cd3..a8b64e7e4 100644 --- a/src/timeline2/view/qml/ClipMenu.qml +++ b/src/timeline2/view/qml/ClipMenu.qml @@ -1,152 +1,152 @@ import QtQuick 2.6 import QtQuick.Controls 1.4 import com.enums 1.0 Menu { id: clipMenu property int clipId property int clipFrame property int clipStatus property int trackId property bool grouped property bool canBeAudio property bool canBeVideo property bool canBeGrouped: timeline.isInSelection(clipId) onAboutToHide: { timeline.ungrabHack() } onAboutToShow: { canBeGrouped = timeline.isInSelection(clipId) } MenuItem { - text: i18n('Copy') + text: i18n("Copy") iconName: 'edit-copy' onTriggered: { root.copiedClip = clipId timeline.copyItem() } } MenuItem { visible: canBeGrouped - text: i18n('Group') + text: i18n("Group") iconName: 'object-group' onTriggered: timeline.triggerAction('group_clip') } MenuItem { visible: grouped - text: i18n('Ungroup') + text: i18n("Ungroup") iconName: 'object-ungroup' onTriggered: timeline.unGroupSelection(clipId) } MenuItem { - text: i18n('Edit Duration') + text: i18n("Edit Duration") iconName: 'measure' onTriggered: { timeline.editItemDuration(clipId) } } MenuItem { visible: root.copiedClip != -1 && root.copiedClip != clipId - text: i18n('Paste Effects') + text: i18n("Paste Effects") iconName: 'edit-paste' onTriggered: timeline.pasteEffects(clipId) } MenuSeparator { visible: true } MenuItem { - text: i18n('Split Audio') + text: i18n("Split Audio") onTriggered: timeline.splitAudio(clipId) visible: !grouped && canBeAudio && clipStatus == ClipState.VideoOnly } MenuItem { - text: i18n('Split Video') + text: i18n("Split Video") onTriggered: timeline.splitVideo(clipId) visible: !grouped && canBeVideo && clipStatus == ClipState.AudioOnly } Menu { - title: i18n('Markers') + title: i18n("Markers") MenuItem { - text: i18n('Add Marker') + text: i18n("Add Marker") onTriggered: timeline.addMarker(clipId) } MenuItem { - text: i18n('Add Marker/Guide quickly') + text: i18n("Add Marker/Guide quickly") onTriggered: timeline.addQuickMarker(clipId) } MenuItem { - text: i18n('Edit Marker') + text: i18n("Edit Marker") onTriggered: timeline.editMarker(clipId) } MenuItem { - text: i18n('Delete Marker') + text: i18n("Delete Marker") onTriggered: timeline.deleteMarker(clipId) } MenuItem { - text: i18n('Delete All Markers') + text: i18n("Delete All Markers") onTriggered: timeline.deleteAllMarkers(clipId) } } MenuItem { - text: i18n('Set Audio Reference') + text: i18n("Set Audio Reference") onTriggered: timeline.setAudioRef(clipId) visible: canBeAudio } MenuItem { - text: i18n('Align Audio') + text: i18n("Align Audio") onTriggered: timeline.alignAudio(clipId) visible: canBeAudio } MenuItem { - text: i18n('Remove') + text: i18n("Remove") iconName: 'edit-delete' onTriggered: timeline.triggerAction('delete_timeline_clip') } MenuItem { visible: true - text: i18n('Extract') + text: i18n("Extract") onTriggered: timeline.extract(clipId) } MenuSeparator { visible: true } MenuItem { visible: true - text: i18n('Change Speed') + text: i18n("Change Speed") onTriggered: { timeline.changeItemSpeed(clipId, -1) } } MenuItem { - text: i18n('Clip in Project Bin') + text: i18n("Clip in Project Bin") onTriggered: timeline.triggerAction('clip_in_project_tree') } MenuItem { visible: true - text: i18n('Split At Playhead') + text: i18n("Split At Playhead") onTriggered: timeline.triggerAction('cut_timeline_clip') } MenuItem { visible: true - text: clipStatus != ClipState.Disabled ? i18n('Disable clip') : i18n('Enable clip') + text: clipStatus != ClipState.Disabled ? i18n("Disable clip") : i18n("Enable clip") onTriggered: timeline.switchEnableState(clipId) } AssetMenu { - title: i18n('Insert an effect...') + title: i18n("Insert an effect...") menuModel: effectModel onAssetSelected: { timeline.addEffectToClip(assetId, clipId) } } AssetMenu { - title: i18n('Insert a composition...') + title: i18n("Insert a composition...") menuModel: transitionModel isTransition: true onAssetSelected: { timeline.addCompositionToClip(assetId, clipId, clipFrame) } } } diff --git a/src/timeline2/view/qml/Composition.qml b/src/timeline2/view/qml/Composition.qml index b4928e880..d19f47bf3 100644 --- a/src/timeline2/view/qml/Composition.qml +++ b/src/timeline2/view/qml/Composition.qml @@ -1,350 +1,350 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * Based on work by Dan Dennedy (Shotcut) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) 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 . * ***************************************************************************/ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import QtQuick.Window 2.2 import 'Timeline.js' as Logic Item { id: compositionRoot property real timeScale: 1.0 property string clipName: '' property string clipResource: '' property string mltService: '' property int modelStart property int displayHeight: 0 property var parentTrack: trackRoot property int inPoint: 0 property int outPoint: 0 property int clipDuration: 0 property bool isAudio: false property bool showKeyframes: false property var itemType: 0 property bool isGrabbed: false property var keyframeModel property bool grouped: false property int binId: 0 property int scrollX: 0 property int trackHeight property int trackIndex //Index in track repeater property int trackId: -42 //Id in the model property int aTrack: -1 property int clipId //Id of the clip in the model property int originalTrackId: trackId property bool isComposition: true property int originalX: x property int originalDuration: clipDuration property int lastValidDuration: clipDuration property int draggedX: x property bool selected: false property double speed: 1.0 property color color: displayRect.color property color borderColor: 'black' signal moved(var clip) signal dragged(var clip, var mouse) signal dropped(var clip) signal draggedToTrack(var clip, int pos, int xpos) signal trimmingIn(var clip, real newDuration, var mouse) signal trimmedIn(var clip) signal trimmingOut(var clip, real newDuration, var mouse) signal trimmedOut(var clip) onKeyframeModelChanged: { if (effectRow.keyframecanvas) { effectRow.keyframecanvas.requestPaint() } } onModelStartChanged: { x = modelStart * timeScale; } onIsGrabbedChanged: { if (compositionRoot.isGrabbed) { compositionRoot.forceActiveFocus(); mouseArea.focus = true } } onTrackIdChanged: { compositionRoot.parentTrack = Logic.getTrackById(trackId) compositionRoot.y = compositionRoot.originalTrackId == -1 || trackId == originalTrackId ? 0 : parentTrack.y - Logic.getTrackById(compositionRoot.originalTrackId).y; } onClipDurationChanged: { width = clipDuration * timeScale; } 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 } /* function reparent(track) { parent = track isAudio = track.isAudio parentTrack = track displayHeight = track.height / 2 compositionRoot.trackId = parentTrack.trackId } */ function updateDrag() { console.log('XXXXXXXXXXXXXXX\n\nXXXXXXXXXXXXX \nUPDATING COMPO DRAG') var itemPos = mapToItem(tracksContainerArea, 0, displayRect.y, displayRect.width, displayRect.height) initDrag(compositionRoot, itemPos, compositionRoot.clipId, compositionRoot.modelStart, compositionRoot.trackId, true) } MouseArea { id: mouseArea anchors.fill: displayRect acceptedButtons: Qt.RightButton hoverEnabled: true Keys.onShortcutOverride: event.accepted = compositionRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) Keys.onLeftPressed: { var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1 controller.requestCompositionMove(compositionRoot.clipId, compositionRoot.originalTrackId, compositionRoot.modelStart - offset, true, true) } Keys.onRightPressed: { var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1 controller.requestCompositionMove(compositionRoot.clipId, compositionRoot.originalTrackId, compositionRoot.modelStart + offset, true, true) } Keys.onUpPressed: { controller.requestCompositionMove(compositionRoot.clipId, controller.getNextTrackId(compositionRoot.originalTrackId), compositionRoot.modelStart, true, true) } Keys.onDownPressed: { controller.requestCompositionMove(compositionRoot.clipId, controller.getPreviousTrackId(compositionRoot.originalTrackId), compositionRoot.modelStart, true, true) } cursorShape: (trimInMouseArea.drag.active || trimOutMouseArea.drag.active)? Qt.SizeHorCursor : dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { root.stopScrolling = true compositionRoot.forceActiveFocus(); if (mouse.button == Qt.RightButton) { if (timeline.selection.indexOf(compositionRoot.clipId) == -1) { timeline.addSelection(compositionRoot.clipId, true) } compositionMenu.clipId = compositionRoot.clipId compositionMenu.grouped = compositionRoot.grouped compositionMenu.trackId = compositionRoot.trackId compositionMenu.popup() } } onReleased: { root.stopScrolling = false } onEntered: { var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height) initDrag(compositionRoot, itemPos, compositionRoot.clipId, compositionRoot.modelStart, compositionRoot.trackId, true) } onExited: { endDrag() } onDoubleClicked: { if (mouse.modifiers & Qt.ShiftModifier) { if (keyframeModel && showKeyframes) { // Add new keyframe var xPos = Math.round(mouse.x / timeline.scaleFactor) var yPos = (compositionRoot.height - mouse.y) / compositionRoot.height keyframeModel.addKeyframe(xPos + compositionRoot.inPoint, yPos) } else { timeline.position = compositionRoot.x / timeline.scaleFactor } } else { timeline.editItemDuration(clipId) } } onPositionChanged: { if (parentTrack) { var mapped = parentTrack.mapFromItem(compositionRoot, mouse.x, mouse.y).x root.mousePosChanged(Math.round(mapped / timeline.scaleFactor)) } } onWheel: zoomByWheel(wheel) } Rectangle { id: displayRect anchors.top: compositionRoot.top anchors.right: compositionRoot.right anchors.left: compositionRoot.left anchors.topMargin: displayHeight * 1.2 height: displayHeight * 1.3 color: selected ? 'mediumpurple' : Qt.darker('mediumpurple') - border.color: selected ? activePalette.highlight : grouped ? root.groupColor : borderColor + border.color: selected ? root.selectionColor : grouped ? root.groupColor : borderColor border.width: isGrabbed ? 8 : 1.5 opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.5 : 1.0 Item { // clipping container id: container anchors.fill: displayRect anchors.margins:1.5 clip: true Rectangle { // text background id: labelRect color: compositionRoot.aTrack > -1 ? 'yellow' : 'lightgray' anchors.top: container.top width: label.width + 2 height: label.height Text { id: label text: clipName + (compositionRoot.aTrack > -1 ? ' > ' + timeline.getTrackNameFromMltIndex(compositionRoot.aTrack) : '') font.pixelSize: root.baseUnit anchors { top: labelRect.top left: labelRect.left topMargin: 1 leftMargin: 1 } color: 'black' } } KeyframeView { id: effectRow visible: compositionRoot.showKeyframes && compositionRoot.keyframeModel selected: compositionRoot.selected inPoint: 0 outPoint: compositionRoot.clipDuration masterObject: compositionRoot kfrModel: compositionRoot.keyframeModel } } /*Drag.active: mouseArea.drag.active Drag.proposedAction: Qt.MoveAction*/ states: [ State { name: 'normal' when: !compositionRoot.selected PropertyChanges { target: compositionRoot z: 0 } }, State { name: 'selected' when: compositionRoot.selected PropertyChanges { target: compositionRoot z: 1 color: 'mediumpurple' } } ] Rectangle { id: trimIn anchors.left: displayRect.left anchors.leftMargin: 0 height: displayRect.height width: 5 color: isAudio? 'green' : 'lawngreen' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction enabled: !compositionRoot.grouped visible: trimInMouseArea.pressed || (root.activeTool === 0 && !dragProxyArea.drag.active && compositionRoot.width > 4 * width) MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false onPressed: { root.stopScrolling = true compositionRoot.originalX = compositionRoot.x compositionRoot.originalDuration = clipDuration parent.anchors.left = undefined } onReleased: { root.stopScrolling = false parent.anchors.left = displayRect.left compositionRoot.trimmedIn(compositionRoot) parent.opacity = 0 } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((trimIn.x) / timeScale) if (delta < -modelStart) { delta = -modelStart } if (delta !== 0) { var newDuration = compositionRoot.clipDuration - delta compositionRoot.trimmingIn(compositionRoot, newDuration, mouse) } } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } Rectangle { id: trimOut anchors.right: displayRect.right anchors.rightMargin: 0 height: displayRect.height width: 5 color: 'red' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction enabled: !compositionRoot.grouped visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !dragProxyArea.drag.active && compositionRoot.width > 4 * width) MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false onPressed: { root.stopScrolling = true compositionRoot.originalDuration = clipDuration parent.anchors.right = undefined } onReleased: { root.stopScrolling = false parent.anchors.right = displayRect.right compositionRoot.trimmedOut(compositionRoot) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var newDuration = Math.round((parent.x + parent.width) / timeScale) compositionRoot.trimmingOut(compositionRoot, newDuration, mouse) } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } } } diff --git a/src/timeline2/view/qml/CompositionMenu.qml b/src/timeline2/view/qml/CompositionMenu.qml index 99b1b1c2c..18c20b2ba 100644 --- a/src/timeline2/view/qml/CompositionMenu.qml +++ b/src/timeline2/view/qml/CompositionMenu.qml @@ -1,55 +1,55 @@ import QtQuick 2.6 import QtQuick.Controls 1.4 Menu { id: compositionMenu property int clipId property int clipStatus property int trackId property bool grouped onAboutToHide: { timeline.ungrabHack() } function show() { //mergeItem.visible = timeline.mergeClipWithNext(trackIndex, index, true) popup() } MenuItem { visible: !grouped && timeline.selection.length > 1 - text: i18n('Group') + text: i18n("Group") iconName: 'object-group' onTriggered: timeline.groupSelection() } MenuItem { visible: grouped - text: i18n('Ungroup') + text: i18n("Ungroup") iconName: 'object-ungroup' onTriggered: timeline.unGroupSelection(clipId) } MenuItem { - text: i18n('Edit Duration') + text: i18n("Edit Duration") iconName: 'measure' onTriggered: { timeline.editItemDuration(clipId) } } MenuItem { visible: true - text: i18n('Copy') + text: i18n("Copy") iconName: 'edit-copy' onTriggered: { root.copiedClip = clipId timeline.copyItem() } } MenuItem { - text: i18n('Remove') + text: i18n("Remove") iconName: 'edit-delete' onTriggered: timeline.triggerAction('delete_timeline_clip') } MenuSeparator { visible: true } } diff --git a/src/timeline2/view/qml/TrackHead.qml b/src/timeline2/view/qml/TrackHead.qml index 6ef39fb05..d70ffd923 100644 --- a/src/timeline2/view/qml/TrackHead.qml +++ b/src/timeline2/view/qml/TrackHead.qml @@ -1,505 +1,505 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.6 import QtQuick.Controls 1.4 import QtQuick.Controls 2.2 as NEWQML import QtQuick.Controls.Styles 1.2 import QtQuick.Layouts 1.3 Rectangle { id: trackHeadRoot property string trackName property string effectNames property bool isStackEnabled property bool isDisabled property bool collapsed: false property int isComposite property bool isLocked: false property bool isActive: false property bool isAudio property bool showAudioRecord property bool current: false property int myTrackHeight property int trackId : -42 property int collapsedHeight: nameEdit.height + 2 property int iconSize: root.baseUnit * 2 property string trackTag property int thumbsFormat: 0 border.width: 1 border.color: root.frameColor signal clicked() function pulseLockButton() { flashLock.restart(); } color: getTrackColor(isAudio, true) //border.color: selected? 'red' : 'transparent' //border.width: selected? 1 : 0 clip: true state: 'normal' states: [ State { name: 'current' when: trackHeadRoot.current PropertyChanges { target: trackHeadRoot color: selectedTrackColor } }, State { when: !trackHeadRoot.current name: 'normal' PropertyChanges { target: trackHeadRoot color: getTrackColor(isAudio, true) } } ] Keys.onDownPressed: { root.moveSelectedTrack(1) } Keys.onUpPressed: { root.moveSelectedTrack(-1) } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: { parent.clicked() if (mouse.button == Qt.RightButton) { headerMenu.trackId = trackId headerMenu.thumbsFormat = thumbsFormat headerMenu.audioTrack = trackHeadRoot.isAudio headerMenu.recEnabled = trackHeadRoot.showAudioRecord headerMenu.popup() } } onClicked: { parent.forceActiveFocus() nameEdit.visible = false if (mouse.button == Qt.LeftButton) { timeline.showTrackAsset(trackId) } } } ColumnLayout { id: targetColumn width: root.baseUnit / 1.2 height: trackHeadRoot.height Item { width: parent.width Layout.fillHeight: true Layout.topMargin: 4 Layout.bottomMargin: 4 Layout.alignment: Qt.AlignVCenter Rectangle { id: trackTarget color: 'grey' anchors.fill: parent width: height border.width: 0 MouseArea { id: targetArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (trackHeadRoot.isAudio) { if (trackHeadRoot.trackId == timeline.audioTarget) { timeline.audioTarget = -1; } else { timeline.audioTarget = trackHeadRoot.trackId; } } else { if (trackHeadRoot.trackId == timeline.videoTarget) { timeline.videoTarget = -1; } else { timeline.videoTarget = trackHeadRoot.trackId; } } } } NEWQML.ToolTip { visible: targetArea.containsMouse font.pixelSize: root.baseUnit delay: 1500 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text - text: i18n('Click to toggle track as target. Target tracks will receive the inserted clips') + text: i18n("Click to toggle track as target. Target tracks will receive the inserted clips") } } state: 'normalTarget' states: [ State { name: 'target' when: (trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.audioTarget) || (!trackHeadRoot.isAudio && trackHeadRoot.trackId == timeline.videoTarget) PropertyChanges { target: trackTarget color: 'green' } }, State { name: 'noTarget' when: !trackHeadRoot.isLocked && !trackHeadRoot.isDisabled PropertyChanges { target: trackTarget color: 'grey' } } ] transitions: [ Transition { to: '*' ColorAnimation { target: trackTarget; duration: 300 } } ] } } } ColumnLayout { id: trackHeadColumn spacing: 0 anchors.fill: parent anchors.leftMargin: targetColumn.width anchors.topMargin: 0 RowLayout { spacing: 0 Layout.leftMargin: 2 ToolButton { id: expandButton implicitHeight: root.baseUnit * 2 implicitWidth: root.baseUnit * 2 iconName: trackHeadRoot.collapsed ? 'arrow-right' : 'arrow-down' onClicked: { trackHeadRoot.myTrackHeight = trackHeadRoot.collapsed ? Math.max(collapsedHeight * 1.5, controller.getTrackProperty(trackId, "kdenlive:trackheight")) : collapsedHeight } - tooltip: trackLabel.visible? i18n('Minimize') : i18n('Expand') + tooltip: trackLabel.visible? i18n("Minimize") : i18n("Expand") } Item { width: trackTag.contentWidth + 4 height: width Rectangle { id: trackLed color: Qt.darker(trackHeadRoot.color, 0.45) anchors.fill: parent width: height border.width: 0 Text { id: trackTag text: trackHeadRoot.trackTag anchors.fill: parent font.pixelSize: root.baseUnit * 1.5 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } MouseArea { id: tagMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { timeline.switchTrackActive(trackHeadRoot.trackId) } } NEWQML.ToolTip { visible: tagMouseArea.containsMouse font.pixelSize: root.baseUnit delay: 1500 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text - text: i18n('Click to make track active/inactive. Active tracks will react to insert/remove operations') + text: i18n("Click to make track active/inactive. Active tracks will react to insert/remove operations") } } state: 'normalled' states: [ State { name: 'locked' when: trackHeadRoot.isLocked PropertyChanges { target: trackLed color: 'red' } }, State { name: 'active' when: trackHeadRoot.isActive PropertyChanges { target: trackLed color: 'yellow' } }, State { name: 'mute' when: trackHeadRoot.isDisabled PropertyChanges { target: trackLed color: 'orange' } }, State { name: 'inactive' when: !trackHeadRoot.isLocked && !trackHeadRoot.isActive PropertyChanges { target: trackLed color: Qt.darker(trackHeadRoot.color, 0.45) } } ] transitions: [ Transition { to: '*' ColorAnimation { target: trackLed; duration: 300 } } ] } } Item { // Spacer Layout.fillWidth: true } ToolButton { iconName: 'tools-wizard' checkable: true enabled: trackHeadRoot.effectNames != '' checked: enabled && trackHeadRoot.isStackEnabled implicitHeight: trackHeadRoot.iconSize implicitWidth: trackHeadRoot.iconSize onClicked: { timeline.showTrackAsset(trackId) controller.setTrackStackEnabled(trackId, !isStackEnabled) } } ToolButton { id: muteButton implicitHeight: trackHeadRoot.iconSize implicitWidth: trackHeadRoot.iconSize iconName: isAudio ? (isDisabled ? 'kdenlive-hide-audio' : 'kdenlive-show-audio') : (isDisabled ? 'kdenlive-hide-video' : 'kdenlive-show-video') iconSource: isAudio ? (isDisabled ? 'qrc:///pics/kdenlive-hide-audio.svgz' : 'qrc:///pics/kdenlive-show-audio.svgz') : (isDisabled ? 'qrc:///pics/kdenlive-hide-video.svgz' : 'qrc:///pics/kdenlive-show-video.svgz') onClicked: controller.setTrackProperty(trackId, "hide", isDisabled ? (isAudio ? '1' : '2') : '3') - tooltip: isAudio ? (isDisabled? i18n('Unmute') : i18n('Mute')) : (isDisabled? i18n('Show') : i18n('Hide')) + tooltip: isAudio ? (isDisabled? i18n("Unmute") : i18n("Mute")) : (isDisabled? i18n("Show") : i18n("Hide")) } ToolButton { id: lockButton implicitHeight: trackHeadRoot.iconSize implicitWidth: trackHeadRoot.iconSize iconName: isLocked ? 'kdenlive-lock' : 'kdenlive-unlock' iconSource: isLocked ? 'qrc:///pics/kdenlive-lock.svg' : 'qrc:///pics/kdenlive-unlock.svg' onClicked: controller.setTrackLockedState(trackId, !isLocked) - tooltip: isLocked? i18n('Unlock track') : i18n('Lock track') + tooltip: isLocked? i18n("Unlock track") : i18n("Lock track") SequentialAnimation { id: flashLock loops: 1 ScaleAnimator { target: lockButton from: 1 to: 2 duration: 500 } ScaleAnimator { target: lockButton from: 2 to: 1 duration: 500 } } } Layout.rightMargin: 4 } RowLayout { id: recLayout Layout.maximumHeight: showAudioRecord ? -1 : 0 Loader { id: audioVuMeter Layout.fillWidth: true Layout.rightMargin: 2 Layout.leftMargin: 4 visible: showAudioRecord && (trackHeadRoot.height >= 2 * muteButton.height + resizer.height) source: isAudio && showAudioRecord ? "AudioLevels.qml" : "" onLoaded: item.trackId = trackId } } RowLayout { Rectangle { id: trackLabel color: 'transparent' Layout.fillWidth: true radius: 2 border.color: trackNameMouseArea.containsMouse ? activePalette.highlight : 'transparent' height: nameEdit.height visible: (trackHeadRoot.height >= trackLabel.height + muteButton.height + resizer.height + recLayout.height) MouseArea { id: trackNameMouseArea anchors.fill: parent hoverEnabled: true propagateComposedEvents: true onDoubleClicked: { nameEdit.visible = true nameEdit.focus = true nameEdit.selectAll() } onClicked: { trackHeadRoot.clicked() trackHeadRoot.focus = true } onEntered: { if (nameEdit.visible == false && trackName == '') { placeHolder.visible = true } } onExited: { if (placeHolder.visible == true) { placeHolder.visible = false } } } Label { text: trackName anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 4 elide: Qt.ElideRight font.pointSize: root.baseUnit * 0.9 } Label { id: placeHolder visible: false enabled: false - text: i18n('Edit track name') + text: i18n("Edit track name") anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 4 elide: Qt.ElideRight font.pointSize: root.baseUnit * 0.9 } TextField { id: nameEdit visible: false width: parent.width text: trackName font.pointSize: root.baseUnit * 0.9 style: TextFieldStyle { padding.top:0 padding.bottom: 0 background: Rectangle { color: activePalette.base anchors.fill: parent } } onEditingFinished: { controller.setTrackProperty(trackId, "kdenlive:track_name", text) visible = false } } } } Item { // Spacer id: spacer Layout.fillWidth: true Layout.fillHeight: true } } Rectangle { id: resizer height: 4 color: 'red' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction width: trackHeadRoot.width y: trackHeadRoot.height - height MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeVerCursor drag.target: parent drag.axis: Drag.YAxis drag.minimumY: trackHeadRoot.collapsedHeight - resizer.height property double startY property double originalY drag.smoothed: false onPressed: { root.stopScrolling = true startY = mapToItem(null, x, y).y originalY = trackHeadRoot.height // reusing originalX to accumulate delta for bubble help } onReleased: { root.stopScrolling = false if (!trimInMouseArea.containsMouse) { parent.opacity = 0 } if (mouse.modifiers & Qt.ShiftModifier) { timeline.adjustAllTrackHeight(trackHeadRoot.trackId, trackHeadRoot.myTrackHeight) } } onEntered: parent.opacity = 0.3 onExited: parent.opacity = 0 onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { parent.opacity = 0.5 var newHeight = originalY + (mapToItem(null, x, y).y - startY) newHeight = Math.max(collapsedHeight, newHeight) trackHeadRoot.myTrackHeight = newHeight } } } } DropArea { //Drop area for tracks anchors.fill: trackHeadRoot keys: 'kdenlive/effect' property string dropData property string dropSource property int dropRow: -1 onEntered: { dropData = drag.getDataAsString('kdenlive/effect') dropSource = drag.getDataAsString('kdenlive/effectsource') } onDropped: { console.log("Add effect: ", dropData) if (dropSource == '') { // drop from effects list controller.addTrackEffect(trackHeadRoot.trackId, dropData); } else { controller.copyTrackEffect(trackHeadRoot.trackId, dropSource); } dropSource = '' dropRow = -1 drag.acceptProposedAction } } } diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml index 5adeb3ab5..dde94ad9c 100644 --- a/src/timeline2/view/qml/timeline.qml +++ b/src/timeline2/view/qml/timeline.qml @@ -1,1507 +1,1507 @@ import QtQuick 2.6 import QtQml.Models 2.2 import QtQuick.Controls 1.4 as OLD import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQuick.Dialogs 1.2 import Kdenlive.Controls 1.0 import QtQuick.Window 2.2 import 'Timeline.js' as Logic Rectangle { id: root objectName: "timelineview" SystemPalette { id: activePalette } color: activePalette.window property bool validMenu: false property color textColor: activePalette.text signal clipClicked() signal mousePosChanged(int position) signal zoomIn(bool onMouse) signal zoomOut(bool onMouse) FontMetrics { id: fontMetrics font.family: "Arial" } ClipMenu { id: clipMenu } CompositionMenu { id: compositionMenu } function fitZoom() { return scrollView.width / (timeline.duration * 1.1) } function scrollPos() { return scrollView.flickableItem.contentX } function goToStart(pos) { scrollView.flickableItem.contentX = pos } function updatePalette() { root.color = activePalette.window root.textColor = activePalette.text playhead.fillColor = activePalette.windowText ruler.repaintRuler() } function moveSelectedTrack(offset) { var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack) var newTrack = cTrack + offset var max = tracksRepeater.count; if (newTrack < 0) { newTrack = max - 1; } else if (newTrack >= max) { newTrack = 0; } console.log('Setting curr tk: ', newTrack, 'MAX: ',max) timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId } function zoomByWheel(wheel) { if (wheel.modifiers & Qt.AltModifier) { // Seek to next snap if (wheel.angleDelta.x > 0) { timeline.triggerAction('monitor_seek_snap_backward') } else { timeline.triggerAction('monitor_seek_snap_forward') } } else if (wheel.modifiers & Qt.ControlModifier) { root.wheelAccumulatedDelta += wheel.angleDelta.y; // Zoom if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) { root.zoomIn(true); root.wheelAccumulatedDelta = 0; } else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) { root.zoomOut(true); root.wheelAccumulatedDelta = 0; } } else if (wheel.modifiers & Qt.ShiftModifier) { // Vertical scroll var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, trackHeaders.height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height) scrollView.flickableItem.contentY = Math.max(newScroll, 0) } else { // Horizontal scroll var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width)) scrollView.flickableItem.contentX = Math.max(newScroll, 0) } wheel.accepted = true } function continuousScrolling(x) { // This provides continuous scrolling at the left/right edges. if (x > scrollView.flickableItem.contentX + scrollView.width - 50) { scrollTimer.item = clip scrollTimer.backwards = false scrollTimer.start() } else if (x < 50) { scrollView.flickableItem.contentX = 0; scrollTimer.stop() } else if (x < scrollView.flickableItem.contentX + 50) { scrollTimer.item = clip scrollTimer.backwards = true scrollTimer.start() } else { scrollTimer.stop() } } function getTrackYFromId(a_track) { return Logic.getTrackYFromId(a_track) } function getTrackYFromMltIndex(a_track) { return Logic.getTrackYFromMltIndex(a_track) } function getTracksCount() { return Logic.getTracksList() } function getMousePos() { return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor } function getScrollPos() { return scrollView.flickableItem.contentX } function setScrollPos(pos) { return scrollView.flickableItem.contentX = pos } function getCopiedItemId() { return copiedClip } function getMouseTrack() { return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY) } function getTrackColor(audio, header) { var col = activePalette.alternateBase if (audio) { col = Qt.tint(col, "#06FF00CC") } if (header) { col = Qt.darker(col, 1.05) } return col } function clearDropData() { clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false scrollTimer.stop() } function isDragging() { return dragProxy.draggedItem > -1 && dragProxyArea.pressed } 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 } property int headerWidth: timeline.headerWidth() property int activeTool: 0 property real baseUnit: fontMetrics.font.pointSize property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2) property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3) property bool stopScrolling: false property int duration: timeline.duration property color audioColor: timeline.audioColor property color videoColor: timeline.videoColor property color lockedColor: timeline.lockedColor property color selectionColor: timeline.selectionColor property color groupColor: timeline.groupColor property int clipBeingDroppedId: -1 property string clipBeingDroppedData property int droppedPosition: -1 property int droppedTrack: -1 property int clipBeingMovedId: -1 property int spacerGroup: -1 property int spacerFrame: -1 property int spacerClickFrame: -1 property real timeScale: timeline.scaleFactor property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? 10 / Math.sqrt(timeScale) - 0.5 : -1 property var timelineSelection: timeline.selection property int trackHeight property int copiedClip: -1 property int zoomOnMouse: -1 property int viewActiveTrack: timeline.activeTrack property int wheelAccumulatedDelta: 0 readonly property int defaultDeltasPerStep: 120 //onCurrentTrackChanged: timeline.selection = [] onTimeScaleChanged: { if (root.zoomOnMouse >= 0) { scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX) root.zoomOnMouse = -1 } else { scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2)) } //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1 ruler.adjustStepSize() if (dragProxy.draggedItem > -1 && dragProxy.masterObject) { // update dragged item pos dragProxy.masterObject.updateDrag() } } onViewActiveTrackChanged: { var tk = Logic.getTrackById(timeline.activeTrack) if (tk.y < scrollView.flickableItem.contentY) { scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3) } else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) { scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3) } } onActiveToolChanged: { if (root.activeTool == 2) { // Spacer activated endDrag() } else if (root.activeTool == 0) { var tk = getMouseTrack() if (tk < 0) { console.log('........ MOUSE OUTSIDE TRAKS\n\n.........') return } var pos = getMousePos() * timeline.scaleFactor var sourceTrack = Logic.getTrackById(tk) var allowComposition = tracksArea.mouseY- sourceTrack.y > sourceTrack.height / 2 var tentativeItem = undefined if (allowComposition) { tentativeItem = getItemAtPos(tk, pos, true) } if (!tentativeItem) { tentativeItem = getItemAtPos(tk, pos, false) } if (tentativeItem) { tentativeItem.updateDrag() } } } DropArea { //Drop area for compositions width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/composition' onEntered: { console.log("Trying to drop composition") if (clipBeingMovedId == -1) { console.log("No clip being moved") var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame if (track >= 0 && !controller.isAudioTrack(track)) { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') console.log("Trying to insert",track, frame, clipBeingDroppedData) clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false) console.log("id",clipBeingDroppedId) continuousScrolling(drag.x + scrollView.flickableItem.contentX) drag.acceptProposedAction() } else { drag.accepted = false } } } onPositionChanged: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY) if (track !=-1) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping)) if (clipBeingDroppedId >= 0){ if (controller.isAudioTrack(track)) { // Don't allow moving composition to an audio track track = controller.getCompositionTrackId(clipBeingDroppedId) } controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else if (!controller.isAudioTrack(track)) { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } clearDropData() } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getCompositionPosition(clipBeingDroppedId) var track = controller.getCompositionTrackId(clipBeingDroppedId) // we simulate insertion at the final position so that stored undo has correct value controller.requestItemDeletion(clipBeingDroppedId, false) timeline.insertNewComposition(track, frame, clipBeingDroppedData, true) } clearDropData() } } DropArea { //Drop area for bin/clips /** @brief local helper function to handle the insertion of multiple dragged items */ function insertAndMaybeGroup(track, frame, droppedData) { var binIds = droppedData.split(";") if (binIds.length == 0) { return -1 } var id = -1 if (binIds.length == 1) { id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false) } else { var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false) // if the clip insertion succeeded, request the clips to be grouped if (ids.length > 0) { timeline.selectItems(ids) id = ids[0] } } return id } property int fakeFrame: -1 property int fakeTrack: -1 width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/producerslist' onEntered: { if (clipBeingMovedId == -1) { //var track = Logic.getTrackIdFromPos(drag.y) var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY) if (track >= 0 && track < tracksRepeater.count) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId //drag.acceptProposedAction() clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist') console.log('dropped data: ', clipBeingDroppedData) if (controller.normalEdit()) { clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData) } else { // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData) fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping)) fakeTrack = timeline.activeTrack } continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { drag.accepted = false } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } clearDropData() } onPositionChanged: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY) if (track >= 0 && track < tracksRepeater.count) { timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) if (clipBeingDroppedId >= 0){ fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping)) fakeTrack = timeline.activeTrack //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping)) if (controller.normalEdit()) { clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true) } else { // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData) fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping)) fakeTrack = timeline.activeTrack } continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getClipPosition(clipBeingDroppedId) var track = controller.getClipTrackId(clipBeingDroppedId) if (!controller.normalEdit()) { frame = fakeFrame track = fakeTrack } /* We simulate insertion at the final position so that stored undo has correct value * NOTE: even if dropping multiple clips, requesting the deletion of the first one is * enough as internally it will request the group deletion */ controller.requestItemDeletion(clipBeingDroppedId, false) var binIds = clipBeingDroppedData.split(";") if (binIds.length == 1) { if (controller.normalEdit()) { timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false) } else { timeline.insertClipZone(clipBeingDroppedData, track, frame) } } else { if (controller.normalEdit()) { timeline.insertClips(track, frame, binIds, true, true) } else { // TODO console.log('multiple clips insert/overwrite not supported yet') } } fakeTrack = -1 fakeFrame = -1 } clearDropData() } } OLD.Menu { id: menu property int clickedX property int clickedY onAboutToHide: { timeline.ungrabHack() editGuideMenu.visible = false } OLD.MenuItem { - text: i18n('Paste') + 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') + 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') + 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') + 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') + text: i18n("Add Guide") onTriggered: { timeline.switchGuide(timeline.position); } } GuidesMenu { - title: i18n('Go to guide...') + title: i18n("Go to guide...") menuModel: guidesModel enabled: guidesDelegateModel.count > 0 onGuideSelected: { timeline.seekPosition = assetFrame timeline.position = timeline.seekPosition } } OLD.MenuItem { id: editGuideMenu - text: i18n('Edit Guide') + text: i18n("Edit Guide") visible: false onTriggered: { timeline.editGuide(timeline.position); } } AssetMenu { - title: i18n('Insert a composition...') + title: i18n("Insert a composition...") menuModel: transitionModel isTransition: true onAssetSelected: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY) var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) var id = timeline.insertComposition(track, frame, assetId, true) if (id == -1) { compositionFail.open() } } } onAboutToShow: { if (guidesModel.hasMarker(timeline.position)) { // marker at timeline position - addGuideMenu.text = i18n('Remove Guide') + addGuideMenu.text = i18n("Remove Guide") editGuideMenu.visible = true } else { - addGuideMenu.text = i18n('Add Guide') + 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') + text: i18n("Add Guide") onTriggered: { timeline.switchGuide(timeline.position); } } GuidesMenu { - title: i18n('Go to guide...') + title: i18n("Go to guide...") menuModel: guidesModel enabled: guidesDelegateModel.count > 0 onGuideSelected: { timeline.seekPosition = assetFrame timeline.position = timeline.seekPosition } } OLD.MenuItem { id: editGuideMenu2 - text: i18n('Edit Guide') + text: i18n("Edit Guide") visible: false onTriggered: { timeline.editGuide(timeline.position); } } OLD.MenuItem { id: addProjectNote - text: i18n('Add Project Note') + text: i18n("Add Project Note") onTriggered: { timeline.triggerAction('add_project_note') } } onAboutToShow: { if (guidesModel.hasMarker(timeline.position)) { // marker at timeline position - addGuideMenu2.text = i18n('Remove Guide') + addGuideMenu2.text = i18n("Remove Guide") editGuideMenu2.visible = true } else { - addGuideMenu2.text = i18n('Add Guide') + 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') + text: i18n("Add Track") onTriggered: { timeline.addTrack(timeline.activeTrack) } } OLD.MenuItem { - text: i18n('Delete Track') + text: i18n("Delete Track") onTriggered: { timeline.deleteTrack(timeline.activeTrack) } } OLD.MenuItem { visible: headerMenu.audioTrack id: showRec - text: "Show Record Controls" + 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: "Configure Recording" + text: i18n("Configure Recording") onTriggered: { timeline.showConfig(4,2) } } OLD.Menu { - title: i18n('Track thumbnails') + title: i18n("Track thumbnails") visible: !headerMenu.audioTrack OLD.ExclusiveGroup { id: thumbStyle } OLD.MenuItem { - text: "In frame" + text: i18n("In frame") id: inFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2) checkable: true exclusiveGroup: thumbStyle } OLD.MenuItem { - text: "In / out frames" + 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: "All frames" + text: i18n("All frames") id: allFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1) checkable: true exclusiveGroup: thumbStyle } OLD.MenuItem { - text: "No thumbnails" + text: i18n("No thumbnails") id: noFrame onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3) checkable: true exclusiveGroup: thumbStyle } onAboutToShow: { switch(headerMenu.thumbsFormat) { case 3: noFrame.checked = true break case 2: inFrame.checked = true break case 1: allFrame.checked = true break default: inOutFrame.checked = true break } } } } Row { Column { id: headerContainer z: 1 Rectangle { id: cornerstone property bool selected: false // Padding between toolbar and track headers. width: headerWidth height: ruler.height color: 'transparent' //selected? shotcutBlue : activePalette.window border.color: selected? 'red' : 'transparent' border.width: selected? 1 : 0 z: 1 } Flickable { // Non-slider scroll area for the track headers. id: headerFlick contentY: scrollView.flickableItem.contentY width: headerWidth height: 100 interactive: false MouseArea { width: trackHeaders.width height: trackHeaders.height acceptedButtons: Qt.NoButton onWheel: { var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height) scrollView.flickableItem.contentY = Math.max(newScroll, 0) } } Column { id: trackHeaders spacing: 0 Repeater { id: trackHeaderRepeater model: multitrack TrackHead { trackName: model.name thumbsFormat: model.thumbsFormat trackTag: model.trackTag isDisabled: model.disabled isComposite: model.composite isLocked: model.locked isActive: model.trackActive isAudio: model.audio showAudioRecord: model.audioRecord effectNames: model.effectNames isStackEnabled: model.isStackEnabled width: headerWidth current: item === timeline.activeTrack trackId: item height: model.trackHeight onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked collapsed: height <= collapsedHeight onMyTrackHeightChanged: { collapsed = myTrackHeight <= collapsedHeight if (!collapsed) { controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight) controller.setTrackProperty(trackId, "kdenlive:collapsed", "0") } else { controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight) } // hack: change property to trigger transition adjustment root.trackHeight = root.trackHeight === 1 ? 0 : 1 } onClicked: { timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId) //timeline.selectTrackHead(currentTrack) } } } } Column { id: trackHeadersResizer spacing: 0 width: 5 Rectangle { id: resizer height: trackHeaders.height width: 3 x: root.headerWidth - 2 color: 'red' opacity: 0 Drag.active: headerMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: headerMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.minimumX: 2 * baseUnit property double startX property double originalX drag.smoothed: false onPressed: { root.stopScrolling = true } onReleased: { root.stopScrolling = false parent.opacity = 0 } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { parent.opacity = 0.5 headerWidth = Math.max(10, mapToItem(null, x, y).x + 2) timeline.setHeaderWidth(headerWidth) } } } } } } } MouseArea { id: tracksArea property real clickX property real clickY width: root.width - headerWidth height: root.height Keys.onDownPressed: { root.moveSelectedTrack(1) } Keys.onUpPressed: { root.moveSelectedTrack(-1) } // This provides continuous scrubbing and scimming at the left/right edges. hoverEnabled: true acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor onWheel: { if (wheel.modifiers & Qt.AltModifier) { // Alt + wheel = seek to next snap point if (wheel.angleDelta.x > 0) { timeline.triggerAction('monitor_seek_snap_backward') } else { timeline.triggerAction('monitor_seek_snap_forward') } } else { var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1 if (timeline.seekPosition > -1) { timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1) } else { timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1) } timeline.position = timeline.seekPosition } } onPressed: { focus = true if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier)) { clickX = mouseX clickY = mouseY return } if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) { // rubber selection rubberSelect.x = mouse.x + tracksArea.x rubberSelect.y = mouse.y rubberSelect.originX = mouse.x rubberSelect.originY = rubberSelect.y rubberSelect.width = 0 rubberSelect.height = 0 } else if (mouse.button & Qt.LeftButton) { if (root.activeTool === 1) { // razor tool var y = mouse.y - ruler.height + scrollView.flickableItem.contentY timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId) } if (dragProxy.draggedItem > -1) { mouse.accepted = false return } if (root.activeTool === 2 && mouse.y > ruler.height) { // spacer tool var y = mouse.y - ruler.height + scrollView.flickableItem.contentY var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1 spacerGroup = timeline.requestSpacerStartOperation(track, frame) if (spacerGroup > -1) { drag.axis = Drag.XAxis Drag.active = true Drag.proposedAction = Qt.MoveAction spacerClickFrame = frame spacerFrame = controller.getItemPosition(spacerGroup) } } else if (root.activeTool === 0 || mouse.y <= ruler.height) { if (mouse.y > ruler.height) { controller.requestClearSelection(); } timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1) timeline.position = timeline.seekPosition } } else if (mouse.button & Qt.RightButton) { menu.clickedX = mouse.x menu.clickedY = mouse.y if (mouse.y > ruler.height) { timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId menu.popup() } else { // ruler menu rulermenu.popup() } } } property bool scim: false onExited: { scim = false } onPositionChanged: { if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier))) { var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width)) var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height) scrollView.flickableItem.contentX = Math.max(newScroll, 0) scrollView.flickableItem.contentY = Math.max(vertScroll, 0) clickX = mouseX clickY = mouseY return } if (!pressed && !rubberSelect.visible && root.activeTool === 1) { cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX if (mouse.modifiers & Qt.ShiftModifier) { timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) } } if (dragProxy.draggedItem > -1) { mouse.accepted = false return } var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)) root.mousePosChanged(mousePos) ruler.showZoneLabels = mouse.y < ruler.height if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) { // rubber selection rubberSelect.visible = true } if (rubberSelect.visible) { var newX = mouse.x var newY = mouse.y if (newX < rubberSelect.originX) { rubberSelect.x = newX + tracksArea.x rubberSelect.width = rubberSelect.originX - newX } else { rubberSelect.x = rubberSelect.originX + tracksArea.x rubberSelect.width = newX - rubberSelect.originX } if (newY < rubberSelect.originY) { rubberSelect.y = newY rubberSelect.height = rubberSelect.originY - newY } else { rubberSelect.y = rubberSelect.originY rubberSelect.height= newY - rubberSelect.originY } } else if (mouse.buttons === Qt.LeftButton) { if (root.activeTool === 0 || mouse.y < ruler.height) { timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)) timeline.position = timeline.seekPosition } else if (root.activeTool === 2 && spacerGroup > -1) { // Move group var track = controller.getItemTrackId(spacerGroup) var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping)) continuousScrolling(mouse.x + scrollView.flickableItem.contentX) } scim = true } else { scim = false } } onReleased: { if (rubberSelect.visible) { rubberSelect.visible = false var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y)) var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height) if (bottomTrack >= topTrack) { var t = [] for (var i = topTrack; i <= bottomTrack; i++) { t.push(tracksRepeater.itemAt(i).trackInternalId) } var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier); } rubberSelect.y = -1 } else if (mouse.modifiers & Qt.ShiftModifier) { // Shift click, process seek timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1) timeline.position = timeline.seekPosition } if (spacerGroup > -1) { var frame = controller.getItemPosition(spacerGroup) timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame); spacerClickFrame = -1 spacerFrame = -1 spacerGroup = -1 } scim = false } Column { Flickable { // Non-slider scroll area for the Ruler. id: rulercontainer width: root.width - headerWidth height: fontMetrics.font.pixelSize * 2 contentX: scrollView.flickableItem.contentX contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale) interactive: false clip: true Ruler { id: ruler width: rulercontainer.contentWidth height: parent.height Rectangle { id: seekCursor visible: timeline.seekPosition > -1 color: activePalette.highlight width: 4 height: ruler.height opacity: 0.5 x: timeline.seekPosition * timeline.scaleFactor } TimelinePlayhead { id: playhead visible: timeline.position > -1 height: baseUnit width: baseUnit * 1.5 fillColor: activePalette.windowText anchors.bottom: parent.bottom x: timeline.position * timeline.scaleFactor - (width / 2) } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: zoomByWheel(wheel) cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape } } } OLD.ScrollView { id: scrollView width: root.width - headerWidth height: root.height - ruler.height y: ruler.height // Click and drag should seek, not scroll the timeline view flickableItem.interactive: false clip: true Rectangle { id: tracksContainerArea width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale) height: trackHeaders.height //Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height) color: root.color Rectangle { // Drag proxy, responsible for clip / composition move id: dragProxy x: 0 y: 0 width: 0 height: 0 property int draggedItem: -1 property int sourceTrack property int sourceFrame property bool isComposition property int verticalOffset property var masterObject color: 'transparent' //opacity: 0.8 MouseArea { id: dragProxyArea anchors.fill: parent drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false drag.minimumX: 0 property int dragFrame property bool shiftClick: false enabled: root.activeTool == 0 onPressed: { console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++') if (mouse.modifiers & Qt.ControlModifier) { mouse.accepted = false return } if (!timeline.exists(dragProxy.draggedItem)) { endDrag() mouse.accepted = false return } dragFrame = -1 timeline.activeTrack = dragProxy.sourceTrack if (mouse.modifiers & Qt.ShiftModifier) { 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) //endDrag() shiftClick = true return } shiftClick = true } else { if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) { controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true) } shiftClick = false } timeline.showAsset(dragProxy.draggedItem) root.stopScrolling = true 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 if (controller.normalEdit() && (tk != Logic.getTrackIdFromPos(parent.y) || 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(tk, mouseX + parent.x, dragProxy.isComposition) if (tentativeClip && 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 { 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 (!shiftClick && dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) { continuousScrolling(mouse.x + parent.x) var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x root.mousePosChanged(Math.round(mapped / timeline.scaleFactor)) var posx = Math.round((parent.x)/ root.timeScale) var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height) var tId = Logic.getTrackIdFromPos(posy) if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) { if (posx == dragFrame) { return } } if (dragProxy.isComposition) { dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping)) timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem) } else { if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) { var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y); dragProxy.masterObject.parent = dragContainer pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y) dragProxy.masterObject.x = pos.x dragProxy.masterObject.y = pos.y //console.log('bringing item to front') } dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping)) timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem) } var delta = dragFrame - dragProxy.sourceFrame if (delta != 0) { var s = timeline.timecode(Math.abs(delta)) // remove leading zeroes if (s.substring(0, 3) === '00:') s = s.substring(3) s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s bubbleHelp.show(parent.x, ruler.height, s) } else bubbleHelp.hide() } } onReleased: { clipBeingMovedId = -1 root.stopScrolling = false if (!shiftClick && 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, true, false, false) controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , true, true, true) } else { // Fake move, only process final move timeline.endFakeMove(dragProxy.draggedItem, dragFrame, true, true, true) } } dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor dragProxy.sourceFrame = dragFrame bubbleHelp.hide() } } onDoubleClicked: { if (dragProxy.masterObject.keyframeModel) { var newVal = (dragProxy.height - mouseY) / dragProxy.height var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal) } } } } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: zoomByWheel(wheel) cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape } Column { // These make the striped background for the tracks. // It is important that these are not part of the track visual hierarchy; // otherwise, the clips will be obscured by the Track's background. Repeater { model: multitrack id: trackBaseRepeater delegate: Rectangle { width: tracksContainerArea.width border.width: 1 border.color: root.frameColor height: model.trackHeight color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red' } } } Column { id: tracksContainer Repeater { id: tracksRepeater; model: trackDelegateModel } Item { id: dragContainer z: 100 } Repeater { id: guidesRepeater; model: guidesDelegateModel } } Rectangle { id: cursor visible: timeline.position > -1 color: root.textColor width: Math.max(1, 1 * timeline.scaleFactor) opacity: (width > 2) ? 0.5 : 1 height: parent.height x: timeline.position * timeline.scaleFactor } } } } /*CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0 } CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0 anchors.right: parent.right mirrorGradient: true }*/ Rectangle { id: cutLine visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height color: 'red' width: Math.max(1, 1 * timeline.scaleFactor) opacity: (width > 2) ? 0.5 : 1 height: root.height - scrollView.__horizontalScrollBar.height - ruler.height x: 0 //x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX y: ruler.height } } } Rectangle { id: bubbleHelp property alias text: bubbleHelpLabel.text color: root.color //application.toolTipBaseColor width: bubbleHelpLabel.width + 8 height: bubbleHelpLabel.height + 8 radius: 4 states: [ State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} }, State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} } ] state: 'invisible' transitions: [ Transition { from: 'invisible' to: 'visible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } }, Transition { from: 'visible' to: 'invisible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } } ] Label { id: bubbleHelpLabel color: activePalette.text //application.toolTipTextColor anchors.centerIn: parent font.pixelSize: root.baseUnit } function show(x, y, text) { bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height bubbleHelp.text = text if (bubbleHelp.state !== 'visible') bubbleHelp.state = 'visible' } function hide() { bubbleHelp.state = 'invisible' bubbleHelp.opacity = 0 } } Rectangle { id: rubberSelect property int originX property int originY y: -1 color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4) border.color: activePalette.highlight border.width: 1 visible: false } /*DropShadow { source: bubbleHelp anchors.fill: bubbleHelp opacity: bubbleHelp.opacity horizontalOffset: 3 verticalOffset: 3 radius: 8 color: '#80000000' transparentBorder: true fast: true }*/ DelegateModel { id: trackDelegateModel model: multitrack delegate: Track { trackModel: multitrack rootIndex: trackDelegateModel.modelIndex(index) timeScale: timeline.scaleFactor width: tracksContainerArea.width height: trackHeight isAudio: audio trackThumbsFormat: thumbsFormat isCurrentTrack: item === timeline.activeTrack trackInternalId: item z: tracksRepeater.count - index } } DelegateModel { id: guidesDelegateModel model: guidesModel Item { id: guideRoot z: 20 Rectangle { id: guideBase width: 1 height: tracksContainer.height x: model.frame * timeScale; color: model.color } Rectangle { visible: mlabel.visible opacity: 0.7 x: guideBase.x y: mlabel.y radius: 2 width: mlabel.width + 4 height: mlabel.height color: model.color MouseArea { z: 10 anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true property int startX drag.axis: Drag.XAxis drag.target: guideRoot onPressed: { drag.target = guideRoot startX = guideRoot.x } onReleased: { if (startX != guideRoot.x) { timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor) } drag.target = undefined } onPositionChanged: { if (pressed) { var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, root.snapping) guideRoot.x = (frame - model.frame) * timeline.scaleFactor } } drag.smoothed: false onDoubleClicked: { timeline.editGuide(model.frame) drag.target = undefined } onClicked: timeline.position = guideBase.x / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers text: model.comment font.pixelSize: root.baseUnit x: guideBase.x + 2 y: scrollView.flickableItem.contentY color: 'white' } } } Connections { target: timeline onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded() onFrameFormatChanged: ruler.adjustFormat() onSelectionChanged: { if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) { endDrag() } } } // This provides continuous scrolling at the left/right edges. Timer { id: scrollTimer interval: 25 repeat: true triggeredOnStart: true property var item property bool backwards onTriggered: { var delta = backwards? -10 : 10 if (item) item.x += delta scrollView.flickableItem.contentX += delta if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1) stop() } } } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 483e1020c..4a75a2f66 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,2504 +1,2504 @@ /*************************************************************************** * 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 "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "dialogs/speeddialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); m_disablePreview->setEnabled(false); connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording); } TimelineController::~TimelineController() { prepareClose(); } void TimelineController::prepareClose() { // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); } void TimelineController::setTargetTracks(QPair targets) { setVideoTarget(targets.first >= 0 && targets.first < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.first) : -1); setAudioTarget(targets.second >= 0 && targets.second < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.second) : -1); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor auto selection = m_model->getCurrentSelection(); if (selection.empty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection return *(selection.begin()); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } int TimelineController::selectedTrack() const { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) return -1; std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item for (int s : sel) { int tid = m_model->getItemTrackId(s); selected_tracks.push_back({m_model->getTrackPosition(tid), tid}); } // sort by track position std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); return selected_tracks.front().second; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, timelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (!select) { m_model->requestRemoveFromSelection(currentClip); } else { m_model->requestAddToSelection(currentClip, !addToCurrent); } } QList TimelineController::selection() const { if (!m_root) return QList(); std::unordered_set sel = m_model->getCurrentSelection(); QList items; for (int id : sel) { items << id; } return items; } void TimelineController::selectItems(const QList &ids) { std::unordered_set ids_s(ids.begin(), ids.end()); m_model->requestSetSelection(ids_s); } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimum = m_model->getClipPosition(clipId); int clip_duration = m_model->getClipPlaytime(clipId); int position = minimum; if (offset > clip_duration / 2) { position += offset; } else { // Check if we have a composition at beginning std::unordered_set existing = m_model->getTrackById_const(tid)->getCompositionsInRange(minimum, minimum + offset); if (existing.size() > 0) { position += offset; } } position = qMin(minimum + clip_duration - 1, position); int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (duration == 0) { duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); } int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = offset > clip_duration / 2; if (lowerVideoTrackId > 0) { int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position); if (bottomId > 0) { QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimum && position > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { position = bottom.first; duration = test_duration; revert = position > minimum; } } } int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (duration2 > 0) { duration = (duration > 0) ? qMin(duration, duration2) : duration2; } } if (duration < 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); } else if (duration <= 1) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (minimum + clip_duration - position < 3) { position = minimum + clip_duration - duration; } } std::unique_ptr props(nullptr); if (revert) { props = std::make_unique(); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { auto sel = m_model->getCurrentSelection(); if (sel.empty()) { return; } // only need to delete the first item, the others will be deleted in cascade m_model->requestItemDeletion(*sel.begin()); } int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) { auto sel = m_model->getCurrentSelection(); if (sel.empty() || sel.size() > 2) { return -1; } int itemId = *(sel.begin()); if (sel.size() == 2) { int parentGroup = m_model->m_groups->getRootId(itemId); if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { return -1; } } if (!restrictToCurrentPos) { if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { return itemId; } } if (m_model->isClip(itemId)) { int position = timelinePosition(); int start = m_model->getClipPosition(itemId); int end = start + m_model->getClipPlaytime(itemId); if (position >= start && position <= end) { return itemId; } } return -1; } void TimelineController::copyItem() { std::unordered_set selectedIds = m_model->getCurrentSelection(); if (selectedIds.empty()) { return; } int clipId = *(selectedIds.begin()); QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(copyString); m_root->setProperty("copiedClip", clipId); m_model->requestSetSelection(selectedIds); } bool TimelineController::pasteItem(int position, int tid) { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (tid == -1) { tid = getMouseTrack(); } if (position == -1) { position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } return TimelineFunctions::pasteClips(m_model, txt, tid, position); } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int newTid; bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); if (addAVTrack) { int newTid2; int mirrorPos = 0; int mirrorId = m_model->getMirrorAudioTrackId(newTid); if (mirrorId > -1) { mirrorPos = m_model->getTrackMltIndex(mirrorId); } m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true); } m_model->buildTrackCompositing(true); if (audioRecTrack) { m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1")); } } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); if (m_activeTrack == selectedTrackIx) { // Make sure we don't keep an index on a deleted track m_activeTrack = -1; } if (m_model->m_audioTarget == selectedTrackIx) { setAudioTarget(-1); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } m_model->requestTrackDeletion(selectedTrackIx); m_model->buildTrackCompositing(true); if (m_activeTrack == -1) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::showConfig(int page, int tab) { pCore->showConfigDialog(page, tab); } void TimelineController::gotoNextSnap() { int nextSnap = m_model->getNextSnapPos(timelinePosition()); if (nextSnap > timelinePosition()) { setPosition(nextSnap); } } void TimelineController::gotoPreviousSnap() { if (timelinePosition() > 0) { setPosition(m_model->getPreviousSnapPos(timelinePosition())); } } void TimelineController::groupSelection() { const auto selection = m_model->getCurrentSelection(); if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } m_model->requestClearSelection(); m_model->requestClipsGroup(selection); m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { auto ids = m_model->getCurrentSelection(); // ask to unselect if needed m_model->requestClearSelection(); if (cid > -1) { ids.insert(cid); } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } } bool TimelineController::dragOperationRunning() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toBool(); } void TimelineController::setInPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = timelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); if (maximumSpace < INT_MAX) { cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); } } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start != cursorPos) { int size = start + m_model->getItemPlaytime(cid) - cursorPos; m_model->requestItemResize(cid, size, false, true, 0, false); } } } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = timelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos); cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1)); } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start + m_model->getItemPlaytime(cid) != cursorPos) { int size = cursorPos - start; m_model->requestItemResize(cid, size, true, true, 0, false); } } } } } void TimelineController::editMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); if (clip->getMarkerModel()->hasMarker(position)) { GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } else { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); } } void TimelineController::addMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); } void TimelineController::addQuickMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } void TimelineController::deleteMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->removeMarker(pos); } void TimelineController::deleteAllMarkers(int cid) { Q_ASSERT(m_model->isClip(cid)); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); clip->getMarkerModel()->removeAllMarkers(); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = timelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = timelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { QList effectSelection; for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::adjustAllTrackHeight(int trackId, int height) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setSeekPosition(int position) { m_seekPosition = position; emit seekPositionChanged(); } void TimelineController::onSeeked(int position) { m_position = position; emit positionChanged(); if (m_seekPosition > -1 && position == m_seekPosition) { m_seekPosition = -1; emit seekPositionChanged(); } } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect) { std::unordered_set itemsToSelect; if (addToSelect) { itemsToSelect = m_model->getCurrentSelection(); } for (int i = 0; i < tracks.count(); i++) { if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { continue; } auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } m_model->requestSetSelection(itemsToSelect); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = timelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; const auto selection = m_model->getCurrentSelection(); for (int cid : selection) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); } void TimelineController::seekCurrentClip(bool seekToEnd) { const auto selection = m_model->getCurrentSelection(); for (int cid : selection) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) { return; } if (m_position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, timelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(Mlt::Filter *filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } int clipId = getCurrentItem(); if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange(bool resetZones) { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(resetZones); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(true); initializePreview(); } } void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(timelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); if (!res) { pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500); } } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || !m_model->isItem(cid) || m_model->getItemTrackId(cid) == -1) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, double speed) { if (clipId == -1) { clipId = getMainSelectedItem(false, true); } if (clipId == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true))); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true))); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } // speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok); QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0)); if (d->exec() != QDialog::Accepted) { return; } speed = d->getValue(); qDebug() << "requesting speed " << speed; } m_model->requestClipTimeWarp(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode)); QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 0; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("aligned", 0); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != audioTarget() && target_track != videoTarget() && !m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { ++it; continue; } tracks << target_track; ++it; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, {track}, zone, false); } bool TimelineController::insertClipZone(const QString &binId, int tid, int position) { QStringList binIdData = binId.split(QLatin1Char('/')); int in = 0; int out = -1; if (binIdData.size() >= 3) { in = binIdData.at(1).toInt(); out = binIdData.at(2).toInt(); } QString bid = binIdData.first(); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } int aTrack = -1; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = clip->frameDuration() - 1; } if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { aTrack = tid; } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { aTrack = tid; vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1; } else { vTrack = tid; aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1; } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, position, QPoint(in, out), m_model->m_editMode == TimelineMode::OverwriteEdit); } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; if (clip->hasAudio()) { aTrack = audioTarget(); } if (clip->hasVideo()) { vTrack = videoTarget(); } /*if (aTrack == -1 && vTrack == -1) { // No target tracks defined, use active track if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { aTrack = m_activeTrack; vTrack = m_model->getMirrorVideoTrackId(aTrack); } else { vTrack = m_activeTrack; aTrack = m_model->getMirrorAudioTrackId(vTrack); } }*/ int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x()) : -1; } void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(int clipId) { TimelineFunctions::switchEnableState(m_model, clipId); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { int track = m_model->getClipTrackId(clipId); int compoId = -1; if (assetId.isEmpty()) { QStringList compositions = KdenliveSettings::favorite_transitions(); if (compositions.isEmpty()) { pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500); return; } compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); } else { compoId = insertNewComposition(track, clipId, offset, assetId, true); } if (compoId > 0) { m_model->requestSetSelection({compoId}); } } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { qDebug() << "/// ADDING ASSET: " << assetId; m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = *m_model->getCurrentSelection().begin(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); if (m_model->m_groups->isInGroup(clipId)) { std::unordered_set groupIds = m_model->getGroupElements(clipId); // Check that no item is grouped with our audioRef item // TODO m_model->requestClearSelection(); } const QString otherBinId = getClipBinId(clipId); if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId); if (newPos) { bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId), (size_t)m_model->getClipPosition(clipId)); m_audioCorrelator->addChild(envelope); } void TimelineController::switchTrackActive(int trackId) { if (trackId == -1) { trackId = m_activeTrack; } bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); m_model->setTrackLockedState(m_activeTrack, !locked); } else { // Invert track lock const auto ids = m_model->getAllTracksIds(); // count the number of tracks to be locked int toBeLockedCount = std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); }); bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount(); for (const int id : ids) { // leave active track unlocked if (leaveOneUnlocked && id == m_activeTrack) { continue; } bool isLocked = m_model->getTrackById_const(id)->isLocked(); m_model->setTrackLockedState(id, !isLocked); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::selectAll() { std::unordered_set ids; for (auto clp : m_model->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { std::unordered_set ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { std::unordered_set targetIds; if (targetId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } for (int s : sel) { if (m_model->isGroup(s)) { std::unordered_set sub = m_model->m_groups->getLeaves(s); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(s)) { targetIds.insert(s); } } } else { if (m_model->m_groups->isInGroup(targetId)) { targetId = m_model->m_groups->getRootId(targetId); } if (m_model->isGroup(targetId)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetId); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(targetId)) { targetIds.insert(targetId); } } if (targetIds.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (txt.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); if (clips.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); for (int i = 1; i < clips.size(); i++) { QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); QDomNodeList subs = subeffects.childNodes(); while (!subs.isEmpty()) { subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); effects.appendChild(subs.at(0)); } } bool result = true; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); result = result && destStack->fromXml(effects, undo, redo); if (!result) { break; } } if (result) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { if (id == -1) { id = getMainSelectedItem(false, true); } if (id == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); // nothing selected emit showItemEffectStack(QString(), nullptr, QSize(), false); return; } std::shared_ptr clip(nullptr); int item = *m_model->getCurrentSelection().begin(); if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) { showAsset(item); } if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = isInSelection(item); } else if (actionData == QLatin1Char('U')) { enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item)); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { if (m_model->getCurrentSelection().empty()) { // TODO: error displayMessage return; } int id = *m_model->getCurrentSelection().begin(); while (m_model->isGroup(id)) { id = *m_model->m_groups->getLeaves(id).begin(); } if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = m_model->m_allClips[itemId]->getFakeTrackId(); return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { - res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo); + res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, false); } res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); - ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo); + ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QList processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, false); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, updateView, finalMove, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { return m_model->getCurrentSelection().count(itemId) > 0; } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable) { TimelineFunctions::enableMultitrackView(m_model, enable); } void TimelineController::saveTimelineSelection(const QDir &targetDir) { TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } bool TimelineController::darkBackground() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5; } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::LinkText).color(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::lockedColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NegativeText).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::selectionColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::NeutralText).color(); } void TimelineController::switchRecording(int trackId) { if (!pCore->isMediaCapturing()) { qDebug() << "start recording" << trackId; if (!m_model->isTrack(trackId)) { qDebug() << "ERROR: Starting to capture on invalid track " << trackId; } if (m_model->getTrackById_const(trackId)->isLocked()) { pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); return; } m_recordStart.first = timelinePosition(); m_recordTrack = trackId; int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); if (maximumSpace == INT_MAX) { m_recordStart.second = 0; } else { m_recordStart.second = maximumSpace - m_recordStart.first; if (m_recordStart.second < 8) { pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " "choose a different track"), ErrorMessage, 500); return; } } pCore->monitorManager()->slotSwitchMonitors(false); pCore->startMediaCapture(true, false); pCore->monitorManager()->slotPlay(); } else { pCore->stopMediaCapture(true, false); pCore->monitorManager()->slotPause(); } } void TimelineController::finishRecording(const QString &recordedFile) { if (recordedFile.isEmpty()) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::function callBack = [this](const QString &binId) { int id = -1; if (m_recordTrack == -1) { return; } qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; bool res = false; if (m_recordStart.second > 0) { // Limited space on track std::shared_ptr clip = pCore->bin()->getBinClip(binId); if (!clip) { return; } int out = qMin((int)clip->frameDuration() - 1, m_recordStart.second - 1); QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out); res = m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); } else { res = m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); } }; QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack); if (binId != QStringLiteral("-1")) { pCore->pushUndo(undo, redo, i18n("Record audio")); } }