diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml index deda5b01e..3b0c4b629 100644 --- a/src/timeline2/view/qml/Clip.qml +++ b/src/timeline2/view/qml/Clip.qml @@ -1,961 +1,949 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.11 import QtQuick.Controls 2.4 import Kdenlive.Controls 1.0 import QtQml.Models 2.11 import QtQuick.Window 2.2 import 'Timeline.js' as Logic import com.enums 1.0 Rectangle { id: clipRoot property real timeScale: 1.0 property string clipName: '' property string clipResource: '' property string mltService: '' property string effectNames property int modelStart property real scrollX: 0 property int inPoint: 0 property int outPoint: 0 property int clipDuration: 0 property int maxDuration: 0 property bool isAudio: false property int audioChannels property bool showKeyframes: false property bool isGrabbed: false property bool grouped: false property var markers property var keyframeModel property int clipStatus: 0 property int itemType: 0 property int fadeIn: 0 property int fadeOut: 0 property int binId: 0 property int positionOffset: 0 property var parentTrack property int trackIndex //Index in track repeater property int clipId //Id of the clip in the model property int trackId: -1 // Id of the parent track in the model property int fakeTid: -1 property int fakePosition: 0 property int originalTrackId: -1 property int originalX: x property int originalDuration: clipDuration property int lastValidDuration: clipDuration property int draggedX: x property bool selected: false property bool isLocked: parentTrack && parentTrack.isLocked == true property bool hasAudio property bool canBeAudio property bool canBeVideo property double speed: 1.0 property color borderColor: 'black' property bool forceReloadThumb property bool isComposition: false property bool hideClipViews property var groupTrimData property int scrollStart: scrollView.flickableItem.contentX - clipRoot.modelStart * timeline.scaleFactor width : clipDuration * timeScale; opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0 signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim, bool controlTrim) signal trimmedIn(var clip, bool shiftTrim, bool controlTrim) signal initGroupTrim(var clip) signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim, bool controlTrim) signal trimmedOut(var clip, bool shiftTrim, bool controlTrim) onScrollStartChanged: { clipRoot.hideClipViews = scrollStart > width || scrollStart + scrollView.viewport.width < 0 } onIsGrabbedChanged: { if (clipRoot.isGrabbed) { grabItem() } else { mouseArea.focus = false } } function grabItem() { clipRoot.forceActiveFocus() mouseArea.focus = true } function clearAndMove(offset) { controller.requestClearSelection() controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - offset, true, true, true) controller.requestAddToSelection(clipRoot.clipId) } onClipResourceChanged: { if (itemType == ProducerType.Color) { color: Qt.darker(getColor(), 1.5) } } ToolTip { visible: mouseArea.containsMouse && !dragProxyArea.pressed delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text font.pointSize: root.fontUnit text: label.text + ' (' + timeline.simplifiedTC(clipRoot.inPoint) + '-' + timeline.simplifiedTC(clipRoot.outPoint) + ')' } } onKeyframeModelChanged: { if (effectRow.keyframecanvas) { console.log('keyframe model changed............') effectRow.keyframecanvas.requestPaint() } } onClipDurationChanged: { width = clipDuration * timeScale; if (parentTrack && parentTrack.isAudio && thumbsLoader.item) { // Duration changed, we may need a different number of repeaters thumbsLoader.item.reload() } } onModelStartChanged: { x = modelStart * timeScale; } onFakePositionChanged: { x = fakePosition * timeScale; } onFakeTidChanged: { if (clipRoot.fakeTid > -1 && parentTrack) { if (clipRoot.parent != dragContainer) { var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y); clipRoot.parent = dragContainer pos = clipRoot.mapFromGlobal(pos.x, pos.y) clipRoot.x = pos.x clipRoot.y = pos.y } clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y } } onForceReloadThumbChanged: { // TODO: find a way to force reload of clip thumbs if (thumbsLoader.item) { thumbsLoader.item.reload() } } onTimeScaleChanged: { x = modelStart * timeScale; width = clipDuration * timeScale; - labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : clipRoot.border.width } onScrollXChanged: { - labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : clipRoot.border.width } border.color: selected ? root.selectionColor : grouped ? root.groupColor : borderColor border.width: isGrabbed ? 8 : 2 function updateDrag() { var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height) initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false) } function getColor() { if (clipStatus == ClipState.Disabled) { return 'grey' } if (itemType == ProducerType.Color) { var color = clipResource.substring(clipResource.length - 9) if (color[0] == '#') { return color } return '#' + color.substring(color.length - 8, color.length - 2) } return isAudio? root.audioColor : root.videoColor } /* function reparent(track) { console.log('TrackId: ',trackId) parent = track height = track.height parentTrack = track trackId = parentTrack.trackId console.log('Reparenting clip to Track: ', trackId) //generateWaveform() } */ property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '') property bool isImage: itemType == ProducerType.Image property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + documentId + '/' + (isImage ? '#0' : '#') DropArea { //Drop area for clips anchors.fill: clipRoot keys: 'kdenlive/effect' property string dropData property string dropSource property int dropRow: -1 onEntered: { dropData = drag.getDataAsString('kdenlive/effect') dropSource = drag.getDataAsString('kdenlive/effectsource') } onDropped: { console.log("Add effect: ", dropData) if (dropSource == '') { // drop from effects list controller.addClipEffect(clipRoot.clipId, dropData); } else { controller.copyClipEffect(clipRoot.clipId, dropSource); } dropSource = '' dropRow = -1 drag.acceptProposedAction } } MouseArea { id: mouseArea enabled: root.activeTool === 0 anchors.fill: clipRoot acceptedButtons: Qt.RightButton hoverEnabled: root.activeTool === 0 cursorShape: (trimInMouseArea.drag.active || trimOutMouseArea.drag.active)? Qt.SizeHorCursor : dragProxyArea.cursorShape onPressed: { root.autoScrolling = false if (mouse.button == Qt.RightButton) { if (timeline.selection.indexOf(clipRoot.clipId) == -1) { controller.requestAddToSelection(clipRoot.clipId, true) } clipMenu.clipId = clipRoot.clipId clipMenu.clipStatus = clipRoot.clipStatus clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor) clipMenu.grouped = clipRoot.grouped clipMenu.trackId = clipRoot.trackId clipMenu.canBeAudio = clipRoot.canBeAudio clipMenu.canBeVideo = clipRoot.canBeVideo clipMenu.popup() } } Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down || event.key === Qt.Key_Escape) Keys.onLeftPressed: { var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1 controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - offset, true, true, true); } Keys.onRightPressed: { var offset = event.modifiers === Qt.ShiftModifier ? timeline.fps() : 1 controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + offset, true, true, true); } Keys.onUpPressed: { controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true); } Keys.onDownPressed: { controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true); } Keys.onEscapePressed: { timeline.grabCurrent() //focus = false } onPositionChanged: { var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x root.mousePosChanged(Math.round(mapped / timeline.scaleFactor)) } onEntered: { var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height) initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false) } onExited: { endDrag() } onWheel: zoomByWheel(wheel) Item { // Thumbs container anchors.fill: parent anchors.leftMargin: parentTrack.isAudio ? 0 : clipRoot.border.width anchors.rightMargin: parentTrack.isAudio ? 0 : clipRoot.border.width anchors.topMargin: clipRoot.border.width anchors.bottomMargin: clipRoot.border.width clip: true Loader { id: thumbsLoader asynchronous: true visible: status == Loader.Ready anchors.fill: parent source: clipRoot.hideClipViews ? "" : parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : "" } } Item { // Clipping container id: container anchors.fill: parent - anchors.margins: 1.5 - clip: true - - Rectangle { - // text background - id: labelRect - color: clipRoot.selected ? 'darkred' : '#66000000' - width: label.width + 2 - height: label.height - visible: clipRoot.width > width / 2 - Text { - id: label - text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '') - font.pointSize: root.fontUnit - anchors { - top: labelRect.top - left: labelRect.left - topMargin: 1 - leftMargin: 1 - } - color: 'white' - style: Text.Outline - styleColor: 'black' - } - } - Rectangle { - // Offset info - id: offsetRect - color: 'darkgreen' - width: offsetLabel.width + radius - height: offsetLabel.height - radius: height/3 - x: labelRect.width + 4 - visible: labelRect.visible && positionOffset != 0 - MouseArea { - id: offsetArea - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - onClicked: { - clearAndMove(positionOffset) - } - ToolTip { - visible: offsetArea.containsMouse - delay: 1000 - timeout: 5000 - background: Rectangle { - color: activePalette.alternateBase - border.color: activePalette.light - } - contentItem: Label { - color: activePalette.text - font.pointSize: root.fontUnit - text: positionOffset < 0 ? i18n("Offset: -%1", timeline.simplifiedTC(-positionOffset)) : i18n("Offset: %1", timeline.simplifiedTC(positionOffset)) - } - } - Text { - id: offsetLabel - text: positionOffset - font.pointSize: root.fontUnit - anchors { - horizontalCenter: parent.horizontalCenter - topMargin: 1 - leftMargin: 1 - } - color: 'white' - style: Text.Outline - styleColor: 'black' - } - } - } - Rectangle { - // effects - id: effectsRect - color: '#555555' - width: effectLabel.width + 2 - height: effectLabel.height - x: labelRect.x - anchors.top: labelRect.bottom - visible: labelRect.visible && clipRoot.effectNames != '' - Text { - id: effectLabel - text: clipRoot.effectNames - font.pointSize: root.fontUnit - anchors { - top: effectsRect.top - left: effectsRect.left - topMargin: 1 - leftMargin: 1 - // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 - } - color: 'white' - //style: Text.Outline - styleColor: 'black' - } - } + anchors.margins: clipRoot.border.width + //clip: true + property bool showDetails: !clipRoot.selected || !effectRow.visible Repeater { + // Clip markers model: markers delegate: Item { anchors.fill: parent Rectangle { id: markerBase width: 1 height: parent.height - x: clipRoot.speed < 0 ? clipRoot.clipDuration * timeScale + (Math.round(model.frame / clipRoot.speed) - (clipRoot.maxDuration - clipRoot.outPoint)) * timeScale : (Math.round(model.frame / clipRoot.speed) - clipRoot.inPoint) * timeScale; + x: clipRoot.speed < 0 ? clipRoot.clipDuration * timeScale + (Math.round(model.frame / clipRoot.speed) - (clipRoot.maxDuration - clipRoot.outPoint)) * timeScale - clipRoot.border.width : (Math.round(model.frame / clipRoot.speed) - clipRoot.inPoint) * timeScale - clipRoot.border.width; color: model.color } Rectangle { visible: mlabel.visible opacity: 0.7 x: markerBase.x radius: 2 width: mlabel.width + 4 height: mlabel.height anchors { bottom: parent.verticalCenter } color: model.color MouseArea { z: 10 anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true onDoubleClicked: timeline.editMarker(clipRoot.clipId, model.frame) onClicked: proxy.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers && parent.width > width * 1.5 text: model.comment font.pointSize: root.fontUnit x: markerBase.x anchors { bottom: parent.verticalCenter topMargin: 2 leftMargin: 2 } color: 'white' } } } + MouseArea { + // Left resize handle + id: trimInMouseArea + anchors.left: container.left + anchors.leftMargin: -clipRoot.border.width + height: parent.height + width: root.baseUnit / 2 + enabled: !isLocked + hoverEnabled: true + drag.target: trimInMouseArea + drag.axis: Drag.XAxis + drag.smoothed: false + property bool shiftTrim: false + property bool controlTrim: false + property bool sizeChanged: false + cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); + onPressed: { + root.autoScrolling = false + clipRoot.originalX = clipRoot.x + clipRoot.originalDuration = clipDuration + anchors.left = undefined + shiftTrim = mouse.modifiers & Qt.ShiftModifier + controlTrim = mouse.modifiers & Qt.ControlModifier + if (!shiftTrim && clipRoot.grouped) { + clipRoot.initGroupTrim(clipRoot) + } + trimIn.opacity = 0 + } + onReleased: { + root.autoScrolling = timeline.autoScroll + anchors.left = parent.left + if (sizeChanged) { + clipRoot.trimmedIn(clipRoot, shiftTrim, controlTrim) + sizeChanged = false + } + } + onPositionChanged: { + if (mouse.buttons === Qt.LeftButton) { + var delta = Math.round(x / timeScale) + if (delta !== 0) { + if (maxDuration > 0 && delta < -inPoint) { + delta = -inPoint + } + var newDuration = clipDuration - delta + sizeChanged = true + clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim, controlTrim) + } + } + } + onEntered: { + if (!pressed) { + trimIn.opacity = 1 + } + } + onExited: { + trimIn.opacity = 0 + } + Rectangle { + id: trimIn + anchors.left: parent.left + width: clipRoot.border.width + height: parent.height + color: 'lawngreen' + opacity: 0 + Drag.active: trimInMouseArea.drag.active + Drag.proposedAction: Qt.MoveAction + visible: trimInMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width) + + ToolTip { + visible: trimInMouseArea.containsMouse && !trimInMouseArea.pressed + delay: 1000 + timeout: 5000 + background: Rectangle { + color: activePalette.alternateBase + border.color: activePalette.light + } + contentItem: Label { + color: activePalette.text + font.pointSize: root.fontUnit + text: i18n("In:%1\nPosition:%2", timeline.simplifiedTC(clipRoot.inPoint),timeline.simplifiedTC(clipRoot.modelStart)) + } + } + } + } + + MouseArea { + // Right resize handle + id: trimOutMouseArea + anchors.right: parent.right + anchors.rightMargin: -clipRoot.border.width + anchors.top: parent.top + height: parent.height + width: root.baseUnit / 2 + hoverEnabled: true + enabled: !isLocked + property bool shiftTrim: false + property bool controlTrim: false + property bool sizeChanged: false + cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); + drag.target: trimOutMouseArea + drag.axis: Drag.XAxis + drag.smoothed: false + + onPressed: { + root.autoScrolling = false + clipRoot.originalDuration = clipDuration + anchors.right = undefined + shiftTrim = mouse.modifiers & Qt.ShiftModifier + controlTrim = mouse.modifiers & Qt.ControlModifier + if (!shiftTrim && clipRoot.grouped) { + clipRoot.initGroupTrim(clipRoot) + } + trimOut.opacity = 0 + } + onReleased: { + root.autoScrolling = timeline.autoScroll + anchors.right = parent.right + if (sizeChanged) { + clipRoot.trimmedOut(clipRoot, shiftTrim, controlTrim) + sizeChanged = false + } + } + onPositionChanged: { + if (mouse.buttons === Qt.LeftButton) { + var newDuration = Math.round((x + width) / timeScale) + if (maxDuration > 0 && newDuration > maxDuration - inPoint) { + newDuration = maxDuration - inPoint + } + if (newDuration != clipDuration) { + sizeChanged = true + clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim, controlTrim) + } + } + } + onEntered: { + if (!pressed) { + trimOut.opacity = 1 + } + } + onExited: trimOut.opacity = 0 + ToolTip { + visible: trimOutMouseArea.containsMouse && !trimOutMouseArea.pressed + delay: 1000 + timeout: 5000 + background: Rectangle { + color: activePalette.alternateBase + border.color: activePalette.light + } + contentItem: Label { + color: activePalette.text + font.pointSize: root.fontUnit + text: i18n("Out: ") + timeline.simplifiedTC(clipRoot.outPoint) + } + } + Rectangle { + id: trimOut + anchors.right: parent.right + width: clipRoot.border.width + height: parent.height + color: 'red' + opacity: 0 + Drag.active: trimOutMouseArea.drag.active + Drag.proposedAction: Qt.MoveAction + visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width) + } + } + KeyframeView { id: effectRow + clip: true + anchors.fill: parent visible: clipRoot.showKeyframes && clipRoot.keyframeModel selected: clipRoot.selected inPoint: clipRoot.inPoint outPoint: clipRoot.outPoint masterObject: clipRoot kfrModel: clipRoot.hideClipViews ? 0 : clipRoot.keyframeModel } + + TimelineTriangle { + // Green fade in triangle + id: fadeInTriangle + fillColor: 'green' + width: Math.min(clipRoot.fadeIn * timeScale, container.width) + height: parent.height + anchors.left: parent.left + anchors.top: parent.top + opacity: 0.4 + } + + TimelineTriangle { + // Red fade out triangle + id: fadeOutCanvas + fillColor: 'red' + width: Math.min(clipRoot.fadeOut * timeScale, container.width) + height: parent.height + anchors.right: parent.right + anchors.top: parent.top + opacity: 0.4 + transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2} + } + + Item { + // Clipping container for clip names + anchors.fill: parent + clip: true + Rectangle { + // Clip name background + id: labelRect + color: clipRoot.selected ? 'darkred' : '#66000000' + y: 0 + width: label.width + 2 * clipRoot.border.width + height: label.height + visible: clipRoot.width > width / 2 + Text { + // Clip name text + id: label + text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '') + font.pointSize: root.fontUnit + anchors { + top: labelRect.top + left: labelRect.left + leftMargin: clipRoot.border.width + } + color: 'white' + //style: Text.Outline + //styleColor: 'black' + } + } + + Rectangle { + // Offset info + id: offsetRect + color: 'darkgreen' + width: offsetLabel.width + radius + height: offsetLabel.height + radius: height/3 + x: labelRect.width + 4 + y: 2 + visible: labelRect.visible && positionOffset != 0 + MouseArea { + id: offsetArea + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + onClicked: { + clearAndMove(positionOffset) + } + ToolTip { + visible: offsetArea.containsMouse + delay: 1000 + timeout: 5000 + background: Rectangle { + color: activePalette.alternateBase + border.color: activePalette.light + } + contentItem: Label { + color: activePalette.text + font.pointSize: root.fontUnit + text: positionOffset < 0 ? i18n("Offset: -%1", timeline.simplifiedTC(-positionOffset)) : i18n("Offset: %1", timeline.simplifiedTC(positionOffset)) + } + } + Text { + id: offsetLabel + text: positionOffset + font.pointSize: root.fontUnit + anchors { + horizontalCenter: parent.horizontalCenter + topMargin: 1 + leftMargin: 1 + } + color: 'white' + style: Text.Outline + styleColor: 'black' + } + } + } + + Rectangle { + // effect names background + id: effectsRect + color: '#555555' + width: effectLabel.width + 2 + height: effectLabel.height + x: labelRect.x + anchors.top: labelRect.bottom + visible: labelRect.visible && clipRoot.effectNames != '' && container.showDetails + Text { + // Effect names text + id: effectLabel + text: clipRoot.effectNames + font.pointSize: root.fontUnit + visible: effectsRect.visible + anchors { + top: effectsRect.top + left: effectsRect.left + leftMargin: 1 + // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 + } + color: 'white' + //style: Text.Outline + styleColor: 'black' + } + } + } } states: [ State { name: 'locked' when: isLocked PropertyChanges { target: clipRoot color: root.lockedColor opacity: 0.8 z: 0 } }, State { name: 'normal' when: clipRoot.selected === false PropertyChanges { target: clipRoot color: Qt.darker(getColor(), 1.5) z: 0 } }, State { name: 'selected' when: clipRoot.selected === true PropertyChanges { target: clipRoot color: getColor() z: 3 } } ] - Rectangle { - id: compositionIn + MouseArea { + // Add start composition area + id: compInArea anchors.left: parent.left anchors.bottom: parent.bottom - anchors.bottomMargin: 2 - anchors.leftMargin: 4 width: root.baseUnit height: width - radius: 2 - color: Qt.darker('mediumpurple') - border.width: 2 - border.color: 'green' - opacity: 0 - enabled: !clipRoot.isAudio && dragProxy.draggedItem === clipRoot.clipId - visible: clipRoot.width > 4 * width - MouseArea { - id: compInArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: parent.opacity = 0.7 - onExited: { - if (!pressed) { - parent.opacity = 0 - } - } - onPressed: { - timeline.addCompositionToClip('', clipRoot.clipId, 0) - } - onReleased: { - parent.opacity = 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + visible: !clipRoot.isAudio + enabled: !clipRoot.isAudio && dragProxy.draggedItem === clipRoot.clipId && compositionIn.visible + onPressed: { + timeline.addCompositionToClip('', clipRoot.clipId, 0) + } + ToolTip { + visible: compInArea.containsMouse && !dragProxyArea.pressed + delay: 1000 + timeout: 5000 + background: Rectangle { + color: activePalette.alternateBase + border.color: activePalette.light } - ToolTip { - visible: compInArea.containsMouse && !dragProxyArea.pressed - delay: 1000 - timeout: 5000 - background: Rectangle { - color: activePalette.alternateBase - border.color: activePalette.light - } - contentItem: Label { - color: activePalette.text - font.pointSize: root.fontUnit - text: i18n("Click to add composition") - } + contentItem: Label { + color: activePalette.text + font.pointSize: root.fontUnit + text: i18n("Click to add composition") } } + Rectangle { + // Start composition box + id: compositionIn + anchors.bottom: parent.bottom + anchors.left: parent.left + width: compInArea.containsMouse ? root.baseUnit : 5 + height: width + radius: width / 2 + visible: clipRoot.width > 4 * parent.width && mouseArea.containsMouse && !dragProxyArea.pressed + color: Qt.darker('mediumpurple') + border.width: 3 + border.color: 'mediumpurple' + Behavior on width { NumberAnimation { duration: 100 } } + } } - Rectangle { - id: compositionOut + + MouseArea { + // Add end composition area + id: compOutArea anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: 2 - anchors.rightMargin: 4 width: root.baseUnit height: width - radius: 2 - color: Qt.darker('mediumpurple') - border.width: 2 - border.color: 'green' - opacity: 0 - enabled: !clipRoot.isAudio && dragProxy.draggedItem == clipRoot.clipId - visible: clipRoot.width > 4 * width - MouseArea { - id: compOutArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - parent.opacity = 0.7 - } - onExited: { - if (!pressed) { - parent.opacity = 0 - } - } - onPressed: { - timeline.addCompositionToClip('', clipRoot.clipId, clipRoot.clipDuration - 1) - } - onReleased: { - parent.opacity = 0 - } - ToolTip { - visible: compOutArea.containsMouse && !dragProxyArea.pressed - delay: 1000 - timeout: 5000 - background: Rectangle { - color: activePalette.alternateBase - border.color: activePalette.light - } - contentItem: Label { - color: activePalette.text - font.pointSize: root.fontUnit - text: i18n("Click to add composition") - } - } - } - } - } - - TimelineTriangle { - id: fadeInTriangle - fillColor: 'green' - width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width) - height: clipRoot.height - clipRoot.border.width * 2 - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: clipRoot.border.width - opacity: 0.3 - } - - TimelineTriangle { - id: fadeOutCanvas - fillColor: 'red' - width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width) - height: clipRoot.height - clipRoot.border.width * 2 - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: clipRoot.border.width - opacity: 0.3 - transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2} - } - - MouseArea { - id: trimInMouseArea - anchors.left: clipRoot.left - anchors.leftMargin: 0 - height: parent.height - width: root.baseUnit / 2 - enabled: !isLocked - hoverEnabled: true - drag.target: trimInMouseArea - drag.axis: Drag.XAxis - drag.smoothed: false - property bool shiftTrim: false - property bool controlTrim: false - property bool sizeChanged: false - cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); - onPressed: { - root.autoScrolling = false - clipRoot.originalX = clipRoot.x - clipRoot.originalDuration = clipDuration - anchors.left = undefined - shiftTrim = mouse.modifiers & Qt.ShiftModifier - controlTrim = mouse.modifiers & Qt.ControlModifier - if (!shiftTrim && clipRoot.grouped) { - clipRoot.initGroupTrim(clipRoot) - } - trimIn.opacity = 0 - } - onReleased: { - root.autoScrolling = timeline.autoScroll - anchors.left = clipRoot.left - if (sizeChanged) { - clipRoot.trimmedIn(clipRoot, shiftTrim, controlTrim) - sizeChanged = false - } - } - onPositionChanged: { - if (mouse.buttons === Qt.LeftButton) { - var delta = Math.round(x / timeScale) - if (delta !== 0) { - if (maxDuration > 0 && delta < -inPoint) { - delta = -inPoint - } - var newDuration = clipDuration - delta - sizeChanged = true - clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim, controlTrim) - } - } - } - onEntered: { - if (!pressed) { - trimIn.opacity = 0.5 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: !clipRoot.isAudio && dragProxy.draggedItem === clipRoot.clipId && compositionOut.visible + visible: !clipRoot.isAudio + onPressed: { + timeline.addCompositionToClip('', clipRoot.clipId, clipRoot.clipDuration - 1) } - } - onExited: { - trimIn.opacity = 0 - } - Rectangle { - id: trimIn - anchors.fill: parent - anchors.margins: 2 - color: isAudio? 'green' : 'lawngreen' - opacity: 0 - Drag.active: trimInMouseArea.drag.active - Drag.proposedAction: Qt.MoveAction - visible: trimInMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width) - ToolTip { - visible: trimInMouseArea.containsMouse && !trimInMouseArea.pressed + visible: compOutArea.containsMouse && !dragProxyArea.pressed delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text font.pointSize: root.fontUnit - text: i18n("In:%1\nPosition:%2", timeline.simplifiedTC(clipRoot.inPoint),timeline.simplifiedTC(clipRoot.modelStart)) - } - } - } - } - - MouseArea { - id: trimOutMouseArea - anchors.right: clipRoot.right - height: parent.height - width: root.baseUnit / 2 - hoverEnabled: true - enabled: !isLocked - property bool shiftTrim: false - property bool controlTrim: false - property bool sizeChanged: false - cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor); - drag.target: trimOutMouseArea - drag.axis: Drag.XAxis - drag.smoothed: false - - onPressed: { - root.autoScrolling = false - clipRoot.originalDuration = clipDuration - anchors.right = undefined - shiftTrim = mouse.modifiers & Qt.ShiftModifier - controlTrim = mouse.modifiers & Qt.ControlModifier - if (!shiftTrim && clipRoot.grouped) { - clipRoot.initGroupTrim(clipRoot) - } - trimOut.opacity = 0 - } - onReleased: { - root.autoScrolling = timeline.autoScroll - anchors.right = clipRoot.right - if (sizeChanged) { - clipRoot.trimmedOut(clipRoot, shiftTrim, controlTrim) - sizeChanged = false - } - } - onPositionChanged: { - if (mouse.buttons === Qt.LeftButton) { - var newDuration = Math.round((x + width) / timeScale) - if (maxDuration > 0 && newDuration > maxDuration - inPoint) { - newDuration = maxDuration - inPoint - } - if (newDuration != clipDuration) { - sizeChanged = true - clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim, controlTrim) + text: i18n("Click to add composition") } } - } - onEntered: { - if (!pressed) { - trimOut.opacity = 0.5 - } - } - onExited: trimOut.opacity = 0 - - ToolTip { - visible: trimOutMouseArea.containsMouse && !trimOutMouseArea.pressed - delay: 1000 - timeout: 5000 - background: Rectangle { - color: activePalette.alternateBase - border.color: activePalette.light - } - contentItem: Label { - color: activePalette.text - font.pointSize: root.fontUnit - text: i18n("Out: ") + timeline.simplifiedTC(clipRoot.outPoint) + Rectangle { + // End composition box + id: compositionOut + anchors.bottom: parent.bottom + anchors.right: parent.right + width: compOutArea.containsMouse ? root.baseUnit : 5 + height: width + radius: width / 2 + visible: clipRoot.width > 4 * parent.width && mouseArea.containsMouse && !dragProxyArea.pressed + color: Qt.darker('mediumpurple') + border.width: 3 + border.color: 'mediumpurple' + Behavior on width { NumberAnimation { duration: 100 } } } } - Rectangle { - id: trimOut - anchors.fill: parent - anchors.margins: 2 - color: 'red' - opacity: 0 - Drag.active: trimOutMouseArea.drag.active - Drag.proposedAction: Qt.MoveAction - visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width) - } - } - - MouseArea { + + MouseArea { + // Fade out drag zone id: fadeOutMouseArea - anchors.right: fadeOutCanvas.left - anchors.rightMargin: -width/2 - anchors.top: fadeOutCanvas.top - anchors.topMargin: -width / 3 + anchors.right: parent.right + anchors.rightMargin: clipRoot.fadeOut <= 0 ? 0 : fadeOutCanvas.width - width / 2 + anchors.top: parent.top width: root.baseUnit height: width - //anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: fadeOutMouseArea drag.axis: Drag.XAxis drag.minimumX: - Math.ceil(width / 2) - drag.maximumX: container.width - Math.floor(width / 4) - visible : clipRoot.width > 3 * width + drag.maximumX: container.width + Math.ceil(width / 4) + visible: clipRoot.width > 3 * width && mouseArea.containsMouse && !dragProxyArea.pressed property int startFadeOut property int lastDuration: -1 - onEntered: fadeOutControl.opacity = 0.7 - onExited: { - if (!pressed) { - fadeOutControl.opacity = 0 + drag.smoothed: false + onClicked: { + if (clipRoot.fadeOut == 0) { + timeline.adjustFade(clipRoot.clipId, 'fadeout', 0, -2) } } - drag.smoothed: false onPressed: { root.autoScrolling = false startFadeOut = clipRoot.fadeOut anchors.right = undefined - fadeOutControl.opacity = 1 + fadeOutCanvas.opacity = 0.6 } onReleased: { - fadeOutCanvas.opacity = 0.3 + fadeOutCanvas.opacity = 0.4 root.autoScrolling = timeline.autoScroll - anchors.right = fadeOutCanvas.left - if (!fadeOutMouseArea.containsMouse) { - fadeOutControl.opacity = 0 - } + anchors.right = parent.right var duration = clipRoot.fadeOut if (duration > 0) { duration += 1 } timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, startFadeOut) bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { - var delta = clipRoot.clipDuration - Math.floor((x + width / 2)/ timeScale) + var delta = clipRoot.clipDuration - Math.floor((x + width / 2 - clipRoot.border.width)/ timeScale) var duration = Math.max(0, delta) duration = Math.min(duration, clipRoot.clipDuration) if (lastDuration != duration) { lastDuration = duration timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1) // Show fade duration as time in a "bubble" help. var s = timeline.simplifiedTC(clipRoot.fadeOut + (clipRoot.fadeOut > 0 ? 1 : 0)) bubbleHelp.show(clipRoot.x + x, parentTrack.y + parentTrack.height, s) } } } Rectangle { id: fadeOutControl - anchors.fill: parent + anchors.top: parent.top + anchors.right: clipRoot.fadeOut > 0 ? undefined : parent.right + anchors.horizontalCenter: clipRoot.fadeOut > 0 ? parent.horizontalCenter : undefined + width: fadeOutMouseArea.containsMouse || Drag.active ? root.baseUnit : 5 + height: width radius: width / 2 - color: '#66FFFFFF' - border.width: 2 + color: 'darkred' + border.width: 3 border.color: 'red' - opacity: 0 enabled: !isLocked && !dragProxy.isComposition Drag.active: fadeOutMouseArea.drag.active + Behavior on width { NumberAnimation { duration: 100 } } Rectangle { id: fadeOutMarker anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: parent.width / 3 color: 'red' - height: container.height - 1 + height: container.height width: 1 + visible : clipRoot.fadeOut > 0 && (fadeOutMouseArea.containsMouse || fadeOutMouseArea.drag.active) } } } - + MouseArea { + // Fade in drag zone id: fadeInMouseArea - anchors.left: fadeInTriangle.right - anchors.leftMargin: -width / 2 - anchors.top: fadeInTriangle.top - anchors.topMargin: -width / 3 + anchors.left: container.left + anchors.leftMargin: clipRoot.fadeIn <= 0 ? 0 : (fadeInTriangle.width - width / 2) + anchors.top: parent.top width: root.baseUnit height: width hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: fadeInMouseArea drag.minimumX: - Math.ceil(width / 2) drag.maximumX: container.width - width / 2 drag.axis: Drag.XAxis drag.smoothed: false property int startFadeIn - onEntered: fadeInControl.opacity = 0.7 - onExited: { - if (!pressed) { - fadeInControl.opacity = 0 + visible: clipRoot.width > 3 * width && mouseArea.containsMouse && !dragProxyArea.pressed + onClicked: { + if (clipRoot.fadeIn == 0) { + timeline.adjustFade(clipRoot.clipId, 'fadein', 0, -2) } } onPressed: { root.autoScrolling = false startFadeIn = clipRoot.fadeIn anchors.left = undefined - fadeInControl.opacity = 1 - fadeInTriangle.opacity = 0.5 + fadeInTriangle.opacity = 0.6 // parentTrack.clipSelected(clipRoot, parentTrack) TODO } onReleased: { root.autoScrolling = timeline.autoScroll - fadeInTriangle.opacity = 0.3 - if (!fadeInMouseArea.containsMouse) { - fadeInControl.opacity = 0 - } - anchors.left = fadeInTriangle.right - console.log('released fade: ', clipRoot.fadeIn) + fadeInTriangle.opacity = 0.4 timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn) bubbleHelp.hide() + anchors.left = container.left } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((x + width / 2) / timeScale) var duration = Math.max(0, delta) duration = Math.min(duration, clipRoot.clipDuration - 1) if (duration != clipRoot.fadeIn) { timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1) // Show fade duration as time in a "bubble" help. var s = timeline.simplifiedTC(clipRoot.fadeIn) bubbleHelp.show(clipRoot.x + x, parentTrack.y + parentTrack.height, s) } } } Rectangle { id: fadeInControl - anchors.fill: parent + anchors.top: parent.top + anchors.left: clipRoot.fadeIn > 0 ? undefined : parent.left + anchors.horizontalCenter: clipRoot.fadeIn > 0 ? parent.horizontalCenter : undefined + width: fadeInMouseArea.containsMouse || Drag.active ? root.baseUnit : 5 + height: width radius: width / 2 - color: '#FF66FFFF' - border.width: 2 - border.color: 'green' + color: 'green' + border.width: 3 + border.color: '#FF66FFFF' enabled: !isLocked && !dragProxy.isComposition - opacity: 0 - visible : clipRoot.width > 3 * width Drag.active: fadeInMouseArea.drag.active + Behavior on width { NumberAnimation { duration: 100 } } Rectangle { id: fadeInMarker anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: parent.width / 3 - color: 'green' - height: container.height - 1 + color: '#FF66FFFF' + height: container.height width: 1 + visible : clipRoot.fadeIn > 0 && (fadeInMouseArea.containsMouse || fadeInMouseArea.drag.active) } } } - - /*MenuItem { - id: mergeItem - text: i18n("Merge with next clip") - onTriggered: timeline.mergeClipWithNext(trackIndex, index, false) - } - MenuItem { - text: i18n("Rebuild Audio Waveform") - onTriggered: timeline.remakeAudioLevels(trackIndex, index) - }*/ - /*onPopupVisibleChanged: { - if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) { - // Try to fix menu running off screen. This only works intermittently. - menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40)) - menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width)) - } - }*/ + } } diff --git a/src/timeline2/view/qml/KeyframeView.qml b/src/timeline2/view/qml/KeyframeView.qml index 311eb1ae6..472c313f1 100644 --- a/src/timeline2/view/qml/KeyframeView.qml +++ b/src/timeline2/view/qml/KeyframeView.qml @@ -1,297 +1,299 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ import QtQuick 2.11 import QtQuick.Controls 1.4 import QtQml.Models 2.11 Rectangle { property int kfrCount : keyframes.count anchors.fill: parent color: Qt.rgba(1,1,0.8, 0.3) id: keyframeContainer property int activeFrame property int activeIndex property int inPoint property int outPoint property bool selected property var masterObject property var kfrModel onKfrCountChanged: { keyframecanvas.requestPaint() } Keys.onShortcutOverride: { if (event.key == Qt.Key_Left) { if (event.modifiers & Qt.AltModifier) { activeFrame = keyframes.itemAt(Math.max(0, --activeIndex)).frame } else { var oldFrame = activeFrame activeFrame -= 1 if (activeFrame < 0) { activeFrame = 0 } else { timeline.updateEffectKeyframe(masterObject.clipId, oldFrame, activeFrame) } } event.accepted = true } else if (event.key == Qt.Key_Right) { if (event.modifiers & Qt.AltModifier) { activeFrame = keyframes.itemAt(Math.min(keyframes.count - 1, ++activeIndex)).frame } else { var oldFrame = activeFrame activeFrame += 1 activeFrame = Math.min(activeFrame, parent.width / timeScale) if (activeFrame > oldFrame) { timeline.updateEffectKeyframe(masterObject.clipId, oldFrame, activeFrame) } } event.accepted = true } else if (event.key == Qt.Key_Return || event.key == Qt.Key_Escape) { keyframeContainer.focus = false event.accepted = true } if ((event.key == Qt.Key_Plus) && !(event.modifiers & Qt.ControlModifier)) { var newVal = Math.min(keyframes.itemAt(activeIndex).value / parent.height + .05, 1) kfrModel.updateKeyframe(activeFrame, newVal) event.accepted = true } else if ((event.key == Qt.Key_Minus) && !(event.modifiers & Qt.ControlModifier)) { var newVal = Math.max(keyframes.itemAt(activeIndex).value / parent.height - .05, 0) kfrModel.updateKeyframe(activeFrame, newVal) event.accepted = true } else { event.accepted = false } } Item { // Keyframes container anchors.fill: parent visible: keyframeContainer.selected && keyframeContainer.width > root.baseUnit * 3 && (kfrCount < (keyframeContainer.width / root.baseUnit)) Repeater { id: keyframes model: kfrModel Rectangle { id: keyframe property int frame : model.frame property int frameType : model.type x: (model.frame - inPoint) * timeScale height: parent.height property int value: parent.height * model.normalizedValue property int tmpVal : keyframeVal.y + root.baseUnit / 2 property int tmpPos : x + keyframeVal.x + root.baseUnit / 2 property int dragPos : -1 anchors.bottom: parent.bottom onFrameTypeChanged: { keyframecanvas.requestPaint() } onValueChanged: { keyframecanvas.requestPaint() } width: Math.max(1, timeScale) color: kfMouseArea.containsMouse ? 'darkred' : 'transparent' MouseArea { id: kfMouseArea anchors.fill: parent anchors.leftMargin: - root.baseUnit/3 anchors.rightMargin: - root.baseUnit/3 hoverEnabled: true cursorShape: Qt.SizeHorCursor + visible: x > root.baseUnit && x < keyframeContainer.width - root.baseUnit drag.target: parent drag.smoothed: false drag.axis: Drag.XAxis onReleased: { root.autoScrolling = timeline.autoScroll dragPos = -1 var newPos = Math.round(parent.x / timeScale) + inPoint if (frame != inPoint && newPos != frame) { if (mouse.modifiers & Qt.ShiftModifier) { // offset all subsequent keyframes // TODO: rewrite using timeline to ensure all kf parameters are updated timeline.offsetKeyframes(masterObject.clipId, frame, newPos) } else { timeline.updateEffectKeyframe(masterObject.clipId, frame, newPos) } } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { if (frame == inPoint) { parent.x = inPoint * timeScale return } var newPos = Math.min(Math.round(parent.x / timeScale), Math.round(keyframeContainer.width / timeScale) - 1) if (newPos < 1) { newPos = 1 } if (newPos != dragPos) { dragPos = newPos parent.x = newPos * timeScale keyframecanvas.requestPaint() } else { parent.x = dragPos * timeScale } } } } Rectangle { id: keyframeVal x: - root.baseUnit / 2 y: keyframeContainer.height - keyframe.value - root.baseUnit / 2 width: root.baseUnit height: width radius: width / 2 color: keyframeContainer.activeFrame == keyframe.frame ? 'red' : kf1MouseArea.containsMouse || kf1MouseArea.pressed ? root.textColor : root.videoColor border.color: kf1MouseArea.containsMouse || kf1MouseArea.pressed ? activePalette.highlight : root.textColor MouseArea { id: kf1MouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: parent drag.smoothed: false onPressed: { drag.axis = (mouse.modifiers & Qt.ShiftModifier) ? Drag.YAxis : Drag.XAndYAxis } onClicked: { keyframeContainer.activeFrame = frame keyframeContainer.activeIndex = index keyframeContainer.focus = true } onReleased: { root.autoScrolling = timeline.autoScroll var newPos = frame == inPoint ? inPoint : Math.round((keyframe.x + parent.x + root.baseUnit / 2) / timeScale) + inPoint if (newPos == frame && keyframe.value == keyframe.height - parent.y - root.baseUnit / 2) { var pos = masterObject.modelStart + frame - inPoint if (proxy.position != pos) { proxy.position = pos } return } var newVal = (keyframeContainer.height - (parent.y + mouse.y)) / keyframeContainer.height if (newVal > 1.5 || newVal < -0.5) { if (frame != inPoint) { timeline.removeEffectKeyframe(masterObject.clipId, frame); } else { if (newVal < 0) { newVal = 0; } else if (newVal > 1) { newVal = 1; } timeline.updateEffectKeyframe(masterObject.clipId, frame, frame, newVal) } } else { if (newVal < 0) { newVal = 0; } else if (newVal > 1) { newVal = 1; } timeline.updateEffectKeyframe(masterObject.clipId, frame, frame == inPoint ? frame : newPos, newVal) } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { if (frame == inPoint) { parent.x = - root.baseUnit / 2 } else { var newPos = Math.min(Math.round((parent.x + root.baseUnit / 2) / timeScale), Math.round(keyframeContainer.width / timeScale) - frame + inPoint - 1) if (frame + newPos <= inPoint) { newPos = inPoint + 1 - frame } if (newPos != dragPos) { dragPos = newPos parent.x = newPos * timeScale - root.baseUnit / 2 keyframecanvas.requestPaint() } else { parent.x = dragPos * timeScale - root.baseUnit / 2 } } keyframecanvas.requestPaint() } } onDoubleClicked: { timeline.removeEffectKeyframe(masterObject.clipId, frame); } } } } } } Canvas { id: keyframecanvas contextType: "2d" renderStrategy: Canvas.Threaded width: kfrCount > 1 ? parent.width : 0 height: kfrCount > 1 ? parent.height : 0 + opacity: keyframeContainer.selected ? 1 : 0.5 Component { id: comp PathCurve { } } Component { id: compline PathLine { } } property var paths : [] Path { id: myPath startX: 0; startY: parent.height } onPaint: { if (kfrCount < 2) { return } context.beginPath() - context.fillStyle = Qt.rgba(0,0,0.8, 0.4); + context.fillStyle = Qt.rgba(0,0,0.8, 0.5); paths = [] var xpos var ypos for(var i = 0; i < keyframes.count; i++) { var type = i > 0 ? keyframes.itemAt(i-1).frameType : keyframes.itemAt(i).frameType xpos = keyframes.itemAt(i).tmpPos if (type == 0) { // discrete paths.push(compline.createObject(keyframecanvas, {"x": xpos, "y": ypos} )) } ypos = keyframes.itemAt(i).tmpVal if (type < 2) { // linear paths.push(compline.createObject(keyframecanvas, {"x": xpos, "y": ypos} )) } else if (type == 2) { // curve paths.push(comp.createObject(keyframecanvas, {"x": xpos, "y": ypos} )) } } paths.push(compline.createObject(keyframecanvas, {"x": parent.width, "y": ypos} )) paths.push(compline.createObject(keyframecanvas, {"x": parent.width, "y": parent.height} )) myPath.pathElements = paths context.clearRect(0,0, width, height); context.path = myPath; context.closePath() context.fill() } } } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 1f61ce09f..be09ce7fc 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,2709 +1,2714 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "dialogs/speeddialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include "audiomixer/mixermanager.hpp" #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_activeTrack(0) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget); connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget); m_disablePreview->setEnabled(false); connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording); connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged); connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording); } TimelineController::~TimelineController() { prepareClose(); } void TimelineController::prepareClose() { // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection); } void TimelineController::setTargetTracks(bool hasVideo, QList audioTargets) { int videoTrack = -1; QList audioTracks; m_hasVideoTarget = hasVideo; m_hasAudioTarget = !audioTargets.isEmpty(); if (m_hasVideoTarget) { videoTrack = m_model->getFirstVideoTrackIndex(); } if (m_hasAudioTarget) { QList tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { if ((*it)->isAudioTrack()) { tracks << (*it)->getId(); } ++it; } int i = 0; while (i < audioTargets.size() && !tracks.isEmpty()) { audioTracks << tracks.takeLast(); i++; } } emit hasAudioTargetChanged(); emit hasVideoTargetChanged(); if (m_videoTargetActive) { setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack); } if (m_audioTargetActive) { setIntAudioTarget((m_hasAudioTarget && (m_lastAudioTarget.size() == audioTargets.size())) ? m_lastAudioTarget : audioTracks); } } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } int TimelineController::selectedTrack() const { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) return -1; std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item for (int s : sel) { int tid = m_model->getItemTrackId(s); selected_tracks.push_back({m_model->getTrackPosition(tid), tid}); } // sort by track position std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); return selected_tracks.front().second; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (!select) { m_model->requestRemoveFromSelection(currentClip); } else { m_model->requestAddToSelection(currentClip, !addToCurrent); } } QList TimelineController::selection() const { if (!m_root) return QList(); std::unordered_set sel = m_model->getCurrentSelection(); QList items; for (int id : sel) { items << id; } return items; } void TimelineController::selectItems(const QList &ids) { std::unordered_set ids_s(ids.begin(), ids.end()); m_model->requestSetSelection(ids_s); } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimumPos = m_model->getClipPosition(clipId); int clip_duration = m_model->getClipPlaytime(clipId); int endPos = minimumPos + clip_duration; int position = minimumPos; int duration = qMin(clip_duration, pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration())); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = offset > clip_duration / 2; int bottomId = 0; if (lowerVideoTrackId > 0) { bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset); } if (bottomId <= 0) { // No video track underneath if (offset < duration && duration < 2 * clip_duration) { // Composition dropped close to start, keep default composition duration } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) { // Composition dropped close to end, keep default composition duration position = endPos - duration; } else { // Use full clip length for duration duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); } } else { duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position)); QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimumPos) { // Lower clip is after top clip if (position + offset > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { offset -= (bottom.first - position); position = bottom.first; duration = test_duration; revert = position > minimumPos; } } } else if (position >= bottom.first) { // Lower clip is before or at same pos as top clip int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (test_duration > 0) { duration = qMin(test_duration, clip_duration); } } } if (duration < 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); } else if (duration <= 1) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (minimumPos + clip_duration - position < 3) { position = minimumPos + clip_duration - duration; } } QPair finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos); position = finalPos.first; duration = finalPos.second; std::unique_ptr props(nullptr); if (revert) { props = std::make_unique(); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { auto sel = m_model->getCurrentSelection(); if (sel.empty()) { return; } // only need to delete the first item, the others will be deleted in cascade m_model->requestItemDeletion(*sel.begin()); } int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) { auto sel = m_model->getCurrentSelection(); if (sel.empty() || sel.size() > 2) { return -1; } int itemId = *(sel.begin()); if (sel.size() == 2) { int parentGroup = m_model->m_groups->getRootId(itemId); if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { return -1; } } if (!restrictToCurrentPos) { if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { return itemId; } } if (m_model->isClip(itemId)) { int position = pCore->getTimelinePosition(); int start = m_model->getClipPosition(itemId); int end = start + m_model->getClipPlaytime(itemId); if (position >= start && position <= end) { return itemId; } } return -1; } void TimelineController::copyItem() { std::unordered_set selectedIds = m_model->getCurrentSelection(); if (selectedIds.empty()) { return; } int clipId = *(selectedIds.begin()); QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(copyString); m_root->setProperty("copiedClip", clipId); m_model->requestSetSelection(selectedIds); } bool TimelineController::pasteItem(int position, int tid) { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (tid == -1) { tid = getMouseTrack(); } if (position == -1) { position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } return TimelineFunctions::pasteClips(m_model, txt, tid, position); } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } QString TimelineController::framesToClock(int frames) const { return m_model->tractor()->frames_to_time(frames, mlt_time_clock); } QString TimelineController::simplifiedTC(int frames) const { if (KdenliveSettings::frametimecode()) { return QString::number(frames); } QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s; } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int newTid; bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); if (addAVTrack) { int newTid2; int mirrorPos = 0; int mirrorId = m_model->getMirrorAudioTrackId(newTid); if (mirrorId > -1) { mirrorPos = m_model->getTrackMltIndex(mirrorId); } m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true); } if (audioRecTrack) { m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1")); } } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); if (m_activeTrack == -1) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::checkTrackDeletion(int selectedTrackIx) { if (m_activeTrack == selectedTrackIx) { // Make sure we don't keep an index on a deleted track m_activeTrack = -1; } if (m_model->m_audioTarget == selectedTrackIx) { setAudioTarget(-1); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } if (m_lastAudioTarget.contains(selectedTrackIx)) { m_lastAudioTarget.removeAll(selectedTrackIx); emit lastAudioTargetChanged(); } if (m_lastVideoTarget == selectedTrackIx) { m_lastVideoTarget = -1; emit lastVideoTargetChanged(); } } void TimelineController::showConfig(int page, int tab) { pCore->showConfigDialog(page, tab); } void TimelineController::gotoNextSnap() { int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition()); if (nextSnap > pCore->getTimelinePosition()) { setPosition(nextSnap); } } void TimelineController::gotoPreviousSnap() { if (pCore->getTimelinePosition() > 0) { setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition())); } } void TimelineController::groupSelection() { const auto selection = m_model->getCurrentSelection(); if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } m_model->requestClearSelection(); m_model->requestClipsGroup(selection); m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { auto ids = m_model->getCurrentSelection(); // ask to unselect if needed m_model->requestClearSelection(); if (cid > -1) { ids.insert(cid); } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } } bool TimelineController::dragOperationRunning() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toBool(); } void TimelineController::setInPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); if (maximumSpace < INT_MAX) { cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); } } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start != cursorPos) { int size = start + m_model->getItemPlaytime(cid) - cursorPos; m_model->requestItemResize(cid, size, false, true, 0, false); } } } } } void TimelineController::setOutPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos); cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1)); } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start + m_model->getItemPlaytime(cid) != cursorPos) { int size = cursorPos - start; m_model->requestItemResize(cid, size, true, true, 0, false); } } } } } void TimelineController::editMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); if (clip->getMarkerModel()->hasMarker(position)) { GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } else { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); } } void TimelineController::addMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); } void TimelineController::addQuickMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } void TimelineController::deleteMarker(int cid, int position) { Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->removeMarker(pos); } void TimelineController::deleteAllMarkers(int cid) { Q_ASSERT(m_model->isClip(cid)); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); clip->getMarkerModel()->removeAllMarkers(); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = pCore->getTimelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = pCore->getTimelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { QList effectSelection; for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::adjustAllTrackHeight(int trackId, int height) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::setPosition(int position) { // Process seek request emit seeked(position); } void TimelineController::setAudioTarget(int track) { if ((track > -1 && !m_model->isTrack(track)) || !m_hasAudioTarget) { return; } m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setIntAudioTarget(QList tracks) { if ((!tracks.isEmpty() && !m_model->isTrack(tracks.first())) || !m_hasAudioTarget) { return; } qDebug()<<"/// GOT AUDIO TRACKS: "<m_audioTarget = tracks.isEmpty() ? -1 : tracks.first(); emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) { return; } m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { if (track > -1 && !m_model->isTrack(track)) { return; } m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect) { std::unordered_set itemsToSelect; if (addToSelect) { itemsToSelect = m_model->getCurrentSelection(); } for (int i = 0; i < tracks.count(); i++) { if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { continue; } auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } m_model->requestSetSelection(itemsToSelect); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = pCore->getTimelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; const auto selection = m_model->getCurrentSelection(); if (track == -1) { for (int cid : selection) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { QMutexLocker lk(&m_metaMutex); int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); return itemId; } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { QMutexLocker lk(&m_metaMutex); bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); return result; } void TimelineController::seekCurrentClip(bool seekToEnd) { const auto selection = m_model->getCurrentSelection(); for (int cid : selection) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); int position = pCore->getTimelinePosition(); if (in > position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) { return; } if (position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, pCore->getTimelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { + if (initialDuration == -2) { + // Add default fade + duration = pCore->currentDoc()->getFramePos(KdenliveSettings::fade_duration()); + initialDuration = 0; + } if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter.get()); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange(bool resetZones) { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(resetZones); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } bool TimelineController::hasPreviewTrack() const { return (m_timelinePreview && (m_timelinePreview->hasOverlayTrack() || m_timelinePreview->hasPreviewTrack())); } void TimelineController::updatePreviewConnection(bool enable) { if (m_timelinePreview) { if (enable) { m_timelinePreview->enable(); } else { m_timelinePreview->disable(); } } } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(true); initializePreview(); } } void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(pCore->getTimelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = pCore->getTimelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = pCore->getTimelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); if (!res) { pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500); } } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || !m_model->isItem(cid)) { return; } const int tid = m_model->getItemTrackId(cid); if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateTrack(int tid) { if (!m_timelinePreview || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } for (auto clp : m_model->getTrackById_const(tid)->m_allClips) { invalidateItem(clp.first); } } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out); } void TimelineController::changeItemSpeed(int clipId, double speed) { if (clipId == -1) { clipId = getMainSelectedItem(false, true); } if (clipId == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true))); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true))); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0)); if (d->exec() != QDialog::Accepted) { return; } speed = d->getValue(); qDebug() << "requesting speed " << speed; } m_model->requestClipTimeWarp(clipId, speed, true); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode)); QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 0; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("aligned", 0); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { tracks << target_track; } ++it; } if (tracks.isEmpty()) { pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), InformationMessage); } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(pCore->getTimelinePosition() + zone.y() - zone.x()); zone.setX(pCore->getTimelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, {track}, zone, false); } bool TimelineController::insertClipZone(const QString &binId, int tid, int position) { QStringList binIdData = binId.split(QLatin1Char('/')); int in = 0; int out = -1; if (binIdData.size() >= 3) { in = binIdData.at(1).toInt(); out = binIdData.at(2).toInt(); } QString bid = binIdData.first(); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } int aTrack = -1; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = (int)clip->frameDuration() - 1; } if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { aTrack = tid; } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { aTrack = tid; vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1; } else { vTrack = tid; aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1; } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, position, QPoint(in, out + 1), m_model->m_editMode == TimelineMode::OverwriteEdit, false); } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; if (clip->hasAudio()) { aTrack = audioTarget(); } if (clip->hasVideo()) { vTrack = videoTarget(); } /*if (aTrack == -1 && vTrack == -1) { // No target tracks defined, use active track if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { aTrack = m_activeTrack; vTrack = m_model->getMirrorVideoTrackId(aTrack); } else { vTrack = m_activeTrack; aTrack = m_model->getMirrorAudioTrackId(vTrack); } }*/ int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = pCore->getTimelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } if (target_tracks.isEmpty()) { pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), InformationMessage); return -1; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x()) : -1; } void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(std::unordered_set selection) { if (selection.empty()) { selection = m_model->getCurrentSelection(); //clipId = getMainSelectedItem(false, false); } TimelineFunctions::switchEnableState(m_model, selection); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { int track = m_model->getClipTrackId(clipId); int compoId = -1; if (assetId.isEmpty()) { QStringList compositions = KdenliveSettings::favorite_transitions(); if (compositions.isEmpty()) { pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500); return; } compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); } else { compoId = insertNewComposition(track, clipId, offset, assetId, true); } if (compoId > 0) { m_model->requestSetSelection({compoId}); } } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { qDebug() << "/// ADDING ASSET: " << assetId; m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = *m_model->getCurrentSelection().begin(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); if (m_model->m_groups->isInGroup(clipId)) { std::unordered_set groupIds = m_model->getGroupElements(clipId); // Check that no item is grouped with our audioRef item // TODO m_model->requestClearSelection(); } const QString otherBinId = getClipBinId(clipId); if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId); if (newPos) { bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId), (size_t)m_model->getClipPosition(clipId)); m_audioCorrelator->addChild(envelope); } void TimelineController::switchTrackActive(int trackId) { if (trackId == -1) { trackId = m_activeTrack; } bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); m_model->setTrackLockedState(m_activeTrack, !locked); } else { // Invert track lock const auto ids = m_model->getAllTracksIds(); // count the number of tracks to be locked int toBeLockedCount = std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); }); bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount(); for (const int id : ids) { // leave active track unlocked if (leaveOneUnlocked && id == m_activeTrack) { continue; } bool isLocked = m_model->getTrackById_const(id)->isLocked(); m_model->setTrackLockedState(id, !isLocked); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } bool TimelineController::hasAudioTarget() const { return m_hasAudioTarget; } bool TimelineController::hasVideoTarget() const { return m_hasVideoTarget; } bool TimelineController::autoScroll() const { return KdenliveSettings::autoscroll(); } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::selectAll() { std::unordered_set ids; for (auto clp : m_model->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { std::unordered_set ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { std::unordered_set targetIds; if (targetId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } for (int s : sel) { if (m_model->isGroup(s)) { std::unordered_set sub = m_model->m_groups->getLeaves(s); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(s)) { targetIds.insert(s); } } } else { if (m_model->m_groups->isInGroup(targetId)) { targetId = m_model->m_groups->getRootId(targetId); } if (m_model->isGroup(targetId)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetId); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(targetId)) { targetIds.insert(targetId); } } if (targetIds.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (txt.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); if (clips.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); for (int i = 1; i < clips.size(); i++) { QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); QDomNodeList subs = subeffects.childNodes(); while (!subs.isEmpty()) { subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); effects.appendChild(subs.at(0)); } } bool result = true; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); result = result && destStack->fromXml(effects, undo, redo); if (!result) { break; } } if (result) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { if (id == -1) { id = getMainSelectedItem(false, true); } if (id == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); // nothing selected emit showItemEffectStack(QString(), nullptr, QSize(), false); return; } std::shared_ptr clip(nullptr); int item = *m_model->getCurrentSelection().begin(); if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) { showAsset(item); } if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = isInSelection(item); } else if (actionData == QLatin1Char('U')) { enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item)); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('W')) { enableAction = clip != nullptr; if (enableAction) { act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { std::unordered_set ids = m_model->getCurrentSelection(); std::unordered_set items_list; int mainId = -1; for (int i : ids) { if (m_model->isGroup(i)) { std::unordered_set children = m_model->m_groups->getLeaves(i); items_list.insert(children.begin(), children.end()); } else { items_list.insert(i); } } for (int id : items_list) { if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) { mainId = id; continue; } if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); } } if (mainId > -1) { if (m_model->isClip(mainId)) { std::shared_ptr clip = m_model->getClipPtr(mainId); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(mainId)) { std::shared_ptr clip = m_model->getCompositionPtr(mainId); clip->setGrab(!clip->isGrabbed()); } } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = -1; if (m_model->m_editMode != TimelineMode::NormalEdit) { trackId = m_model->m_allClips[itemId]->getFakeTrackId(); } return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo); } res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))}; std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 < p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QList processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { return m_model->getCurrentSelection().count(itemId) > 0; } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable, bool refresh) { TimelineFunctions::enableMultitrackView(m_model, enable, refresh); } void TimelineController::saveTimelineSelection(const QDir &targetDir) { TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } bool TimelineController::darkBackground() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5; } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::LinkText).color(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::PositiveText).color(); } QColor TimelineController::lockedColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NegativeText).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::selectionColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::NeutralText).color(); } void TimelineController::switchRecording(int trackId) { if (!pCore->isMediaCapturing()) { qDebug() << "start recording" << trackId; if (!m_model->isTrack(trackId)) { qDebug() << "ERROR: Starting to capture on invalid track " << trackId; } if (m_model->getTrackById_const(trackId)->isLocked()) { pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); return; } m_recordStart.first = pCore->getTimelinePosition(); m_recordTrack = trackId; int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); if (maximumSpace == INT_MAX) { m_recordStart.second = 0; } else { m_recordStart.second = maximumSpace - m_recordStart.first; if (m_recordStart.second < 8) { pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " "choose a different track"), ErrorMessage, 500); return; } } pCore->monitorManager()->slotSwitchMonitors(false); pCore->startMediaCapture(trackId, true, false); pCore->monitorManager()->slotPlay(); } else { pCore->stopMediaCapture(trackId, true, false); pCore->monitorManager()->slotPause(); } } void TimelineController::finishRecording(const QString &recordedFile) { if (recordedFile.isEmpty()) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::function callBack = [this](const QString &binId) { int id = -1; if (m_recordTrack == -1) { return; } qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; if (m_recordStart.second > 0) { // Limited space on track std::shared_ptr clip = pCore->bin()->getBinClip(binId); if (!clip) { return; } int out = qMin((int)clip->frameDuration() - 1, m_recordStart.second - 1); QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out); m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); } else { m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); } }; QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack); if (binId != QStringLiteral("-1")) { pCore->pushUndo(undo, redo, i18n("Record audio")); } } void TimelineController::updateVideoTarget() { if (videoTarget() > -1) { m_lastVideoTarget = videoTarget(); m_videoTargetActive = true; emit lastVideoTargetChanged(); } else { m_videoTargetActive = false; } } void TimelineController::updateAudioTarget() { if (audioTarget() > -1) { m_lastAudioTarget = {audioTarget()}; m_audioTargetActive = true; emit lastAudioTargetChanged(); } else { m_audioTargetActive = false; } } bool TimelineController::hasActiveTracks() const { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { return true; } ++it; } return false; } void TimelineController::showMasterEffects() { emit showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false); } bool TimelineController::refreshIfVisible(int cid) { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { ++it; continue; } int child = m_model->getClipByPosition(target_track, pCore->getTimelinePosition()); if (child > 0) { if (m_model->m_allClips[child]->binId().toInt() == cid) { return true; } } ++it; } return false; }