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: "<isAudioTrack()) {
int in = getClipPosition(clipId);
emit invalidateZone(in, in + getClipPlaytime(clipId));
}
return true;
};
}
if (old_trackId != -1) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
- ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove);
+ ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
if (!ok) {
qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && 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