diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml index b5f31ddcc..9c6cf5874 100644 --- a/src/timeline2/view/qml/Clip.qml +++ b/src/timeline2/view/qml/Clip.qml @@ -1,666 +1,670 @@ /* * 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.2 import QtQuick.Controls 1.0 import Kdenlive.Controls 1.0 import QtGraphicalEffects 1.0 import QtQml.Models 2.2 import QtQuick.Window 2.2 Rectangle { id: clipRoot property real timeScale: 1.0 property string clipName: '' property string clipResource: '' property string mltService: '' property int modelStart: x + property int scrollX: 0 property int inPoint: 0 property int outPoint: 0 property int clipDuration: 0 property bool isAudio: false property bool isComposition: false property bool grouped: false property var audioLevels property var markers : [] property int fadeIn: 0 property int fadeOut: 0 property int binId: 0 property int trackIndex //Index in track repeater property int trackId: -42 //Id in the model property int clipId //Id of the clip in the model property int originalTrackId: trackId property int originalX: x property int originalDuration: clipDuration property int lastValidDuration: clipDuration property int draggedX: x property bool selected: false property string hash: 'ccc' //TODO property double speed: 1.0 property color borderColor: 'black' x: modelStart * timeScale width : clipDuration * timeScale; signal clicked(var clip, int shiftClick) signal moved(var clip) signal dragged(var clip, var mouse) signal dropped(var clip) signal draggedToTrack(var clip, int pos) signal trimmingIn(var clip, real newDuration, var mouse) signal trimmedIn(var clip) signal trimmingOut(var clip, real newDuration, var mouse) signal trimmedOut(var clip) onClipDurationChanged: { width = clipDuration * timeScale; } onTimeScaleChanged: { width = clipDuration * timeScale; + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 + } + onScrollXChanged: { + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } SystemPalette { id: activePalette } color: Qt.darker(getColor()) border.color: selected? 'red' : borderColor border.width: 1.5 clip: true Drag.active: mouseArea.drag.active Drag.proposedAction: Qt.MoveAction opacity: Drag.active? 0.5 : 1.0 function getColor() { if (mltService === 'color') { //console.log('clip color', clipResource, " / ", '#' + clipResource.substring(2, 8)) if (clipResource.length == 10) { // 0xRRGGBBAA return '#' + clipResource.substring(2, 8) } } return isAudio? 'darkseagreen' : 'blue' //root.shotcutBlue } function reparent(track) { parent = track isAudio = track.isAudio height = track.height y = track.y generateWaveform() } function generateWaveform() { // This is needed to make the model have the correct count. // Model as a property expression is not working in all cases. waveformRepeater.model = Math.ceil(waveform.innerWidth / waveform.maxWidth) for (var i = 0; i < waveformRepeater.count; i++) waveformRepeater.itemAt(0).update() } function imagePath(time) { console.log('get clip thumb for sercvie: ', mltService) if (isAudio || mltService === 'color' || mltService === '') { return '' } else { return 'image://thumbnail/' + binId + '/' + mltService + '/' + clipResource + '#' + time } } onAudioLevelsChanged: generateWaveform() Image { id: outThumbnail visible: timeline.showThumbnails && mltService != 'color' anchors.right: parent.right anchors.top: parent.top anchors.topMargin: parent.border.width anchors.rightMargin: parent.border.width + 1 anchors.bottom: parent.bottom anchors.bottomMargin: parent.border.width + 1 width: height * 16.0/9.0 fillMode: Image.PreserveAspectFit asynchronous: true source: imagePath(outPoint) } Image { id: inThumbnail visible: timeline.showThumbnails && mltService != 'color' anchors.left: parent.left anchors.top: parent.top anchors.topMargin: parent.border.width anchors.bottom: parent.bottom anchors.bottomMargin: parent.border.width + 1 anchors.leftMargin: parent.border.width width: height * 16.0/9.0 fillMode: Image.PreserveAspectFit asynchronous: true source: imagePath(inPoint) } /*TimelineTransition { visible: isComposition anchors.fill: parent property var color: isAudio? 'darkseagreen' : 'blue' //root.shotcutBlue colorA: color colorB: clipRoot.selected ? Qt.darker(color) : Qt.lighter(color) }*/ Row { id: waveform visible: timeline.showAudioThumbnails height: isAudio? parent.height : parent.height / 2 anchors.left: parent.left anchors.bottom: parent.bottom anchors.margins: parent.border.width opacity: 0.7 property int maxWidth: 10000 property int innerWidth: clipRoot.width - clipRoot.border.width * 2 Repeater { id: waveformRepeater TimelineWaveform { width: Math.min(waveform.innerWidth, waveform.maxWidth) height: waveform.height fillColor: 'red' property int channels: 2 inPoint: Math.round((clipRoot.inPoint + index * waveform.maxWidth / timeScale) * speed) * channels outPoint: inPoint + Math.round(width / timeScale * speed) * channels levels: audioLevels } } } Rectangle { // audio peak line width: parent.width - parent.border.width * 2 visible: timeline.showAudioThumbnails height: 1 anchors.left: parent.left anchors.bottom: parent.bottom anchors.leftMargin: parent.border.width anchors.bottomMargin: waveform.height * 0.9 color: Qt.darker(parent.color) opacity: 0.4 } Rectangle { // text background + id: labelRect color: 'lightgray' opacity: 0.7 anchors.top: parent.top - anchors.left: parent.left anchors.topMargin: parent.border.width anchors.leftMargin: parent.border.width // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) width: label.width + 2 height: label.height - } - - Text { - id: label - text: clipName - font.pixelSize: root.baseUnit - anchors { - top: parent.top - left: parent.left - topMargin: parent.border.width + 1 - leftMargin: parent.border.width + 1 + Text { + id: label + text: clipName + font.pixelSize: root.baseUnit + anchors { + top: parent.top + left: parent.left + topMargin: parent.border.width + 1 + leftMargin: parent.border.width // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 + } + color: 'black' } - color: 'black' } Repeater { model: markers.length / 2 delegate: Item { anchors.fill: parent Rectangle { id: markerBase width: 1 // modelDa height: parent.height x: (markers[2 * modelData] - clipRoot.inPoint) * timeScale; color: 'red' } Rectangle { opacity: 0.7 x: markerBase.x radius: 2 width: mlabel.width + 4 height: mlabel.height anchors { bottom: parent.verticalCenter } color: 'red' } Text { id: mlabel text: markers[2 * modelData + 1] font.pixelSize: root.baseUnit x: markerBase.x anchors { bottom: parent.verticalCenter topMargin: 2 leftMargin: 2 } color: 'white' } } } states: [ State { name: 'normal' when: !clipRoot.selected PropertyChanges { target: clipRoot z: 0 } }, State { name: 'selectedBlank' when: clipRoot.selected PropertyChanges { target: clipRoot color: Qt.darker(getColor()) } }, State { name: 'selected' when: clipRoot.selected PropertyChanges { target: clipRoot z: 1 color: getColor() } } ] MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton drag.target: parent drag.axis: Drag.XAxis property int startX onPressed: { root.stopScrolling = true originalX = parent.x originalTrackId = clipRoot.trackId startX = parent.x clipRoot.forceActiveFocus(); clipRoot.clicked(clipRoot, mouse.modifiers === Qt.ShiftModifier) } onPositionChanged: { if (mouse.y < 0 || mouse.y > height) { parent.draggedToTrack(clipRoot, mapToItem(null, 0, mouse.y).y) } else { parent.dragged(clipRoot, mouse) } } onReleased: { root.stopScrolling = false parent.y = 0 var delta = parent.x - startX if (Math.abs(delta) >= 1.0 || trackId !== originalTrackId) { parent.moved(clipRoot) originalX = parent.x originalTrackId = trackId } else { parent.dropped(clipRoot) } } onDoubleClicked: timeline.position = clipRoot.x / timeline.scaleFactor onWheel: zoomByWheel(wheel) MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton propagateComposedEvents: true cursorShape: (trimInMouseArea.drag.active || trimOutMouseArea.drag.active)? Qt.SizeHorCursor : (fadeInMouseArea.drag.active || fadeOutMouseArea.drag.active)? Qt.PointingHandCursor : drag.active? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { root.stopScrolling = true clipRoot.forceActiveFocus(); if (!clipRoot.selected) { clipRoot.clicked(clipRoot, false) } } onClicked: menu.show() } } TimelineTriangle { id: fadeInTriangle width: parent.fadeIn * timeScale height: parent.height - parent.border.width * 2 anchors.left: parent.left anchors.top: parent.top anchors.margins: parent.border.width opacity: 0.5 } Rectangle { id: fadeInControl anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined anchors.top: fadeInTriangle.top anchors.topMargin: -3 width: 15 height: 15 radius: 7.5 color: 'black' border.width: 2 border.color: 'white' opacity: 0 Drag.active: fadeInMouseArea.drag.active MouseArea { id: fadeInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: parent drag.axis: Drag.XAxis property int startX property int startFadeIn onEntered: parent.opacity = 0.7 onExited: parent.opacity = 0 onPressed: { root.stopScrolling = true startX = parent.x startFadeIn = fadeIn parent.anchors.left = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 // trackRoot.clipSelected(clipRoot, trackRoot) TODO } onReleased: { root.stopScrolling = false if (fadeInTriangle.width > parent.radius) parent.anchors.horizontalCenter = fadeInTriangle.right else parent.anchors.left = fadeInTriangle.left bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((parent.x - startX) / timeScale) var duration = startFadeIn + delta timeline.fadeIn(trackIndex, index, duration) // Show fade duration as time in a "bubble" help. var s = timeline.timecode(Math.max(duration, 0)) bubbleHelp.show(clipRoot.x, trackRoot.y + clipRoot.height, s.substring(6)) } } } SequentialAnimation on scale { loops: Animation.Infinite running: fadeInMouseArea.containsMouse NumberAnimation { from: 1.0 to: 0.5 duration: 250 easing.type: Easing.InOutQuad } NumberAnimation { from: 0.5 to: 1.0 duration: 250 easing.type: Easing.InOutQuad } } } TimelineTriangle { id: fadeOutCanvas width: parent.fadeOut * timeScale height: parent.height - parent.border.width * 2 anchors.right: parent.right anchors.top: parent.top anchors.margins: parent.border.width opacity: 0.5 transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2} } Rectangle { id: fadeOutControl anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined anchors.top: fadeOutCanvas.top anchors.topMargin: -3 width: 15 height: 15 radius: 7.5 color: 'black' border.width: 2 border.color: 'white' opacity: 0 Drag.active: fadeOutMouseArea.drag.active MouseArea { id: fadeOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: parent drag.axis: Drag.XAxis property int startX property int startFadeOut onEntered: parent.opacity = 0.7 onExited: parent.opacity = 0 onPressed: { root.stopScrolling = true startX = parent.x startFadeOut = fadeOut parent.anchors.right = undefined parent.anchors.horizontalCenter = undefined parent.opacity = 1 } onReleased: { root.stopScrolling = false if (fadeOutCanvas.width > parent.radius) parent.anchors.horizontalCenter = fadeOutCanvas.left else parent.anchors.right = fadeOutCanvas.right bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((startX - parent.x) / timeScale) var duration = startFadeOut + delta timeline.fadeOut(trackIndex, index, duration) // Show fade duration as time in a "bubble" help. var s = timeline.timecode(Math.max(duration, 0)) bubbleHelp.show(clipRoot.x + clipRoot.width, trackRoot.y + clipRoot.height, s.substring(6)) } } } SequentialAnimation on scale { loops: Animation.Infinite running: fadeOutMouseArea.containsMouse NumberAnimation { from: 1.0 to: 0.5 duration: 250 easing.type: Easing.InOutQuad } NumberAnimation { from: 0.5 to: 1.0 duration: 250 easing.type: Easing.InOutQuad } } } Rectangle { id: trimIn anchors.left: parent.left anchors.leftMargin: 0 height: parent.height width: 5 color: isAudio? 'green' : 'lawngreen' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis onPressed: { root.stopScrolling = true clipRoot.originalX = clipRoot.x clipRoot.originalDuration = clipDuration parent.anchors.left = undefined } onReleased: { root.stopScrolling = false parent.anchors.left = clipRoot.left clipRoot.trimmedIn(clipRoot) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = Math.round((trimIn.x) / timeScale) if (delta !== 0) { var newDuration = clipDuration - delta clipRoot.trimmingIn(clipRoot, newDuration, mouse) } } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } Rectangle { id: trimOut anchors.right: parent.right anchors.rightMargin: 0 height: parent.height width: 5 color: 'red' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis onPressed: { root.stopScrolling = true clipRoot.originalDuration = clipDuration parent.anchors.right = undefined } onReleased: { root.stopScrolling = false parent.anchors.right = clipRoot.right clipRoot.trimmedOut(clipRoot) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var newDuration = Math.round((parent.x + parent.width) / timeScale) clipRoot.trimmingOut(clipRoot, newDuration, mouse) } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } Menu { id: menu function show() { //mergeItem.visible = timeline.mergeClipWithNext(trackIndex, index, true) popup() } MenuItem { visible: true text: i18n('Cut') onTriggered: { if (!trackRoot.isLocked) { timeline.copyClip(trackIndex, index) timeline.remove(trackIndex, index) } else { root.pulseLockButtonOnTrack(currentTrack) } } } MenuItem { visible: !grouped && trackRoot.selection.length > 1 text: i18n('Group') onTriggered: timeline.groupSelection() } MenuItem { visible: grouped text: i18n('Ungroup') onTriggered: timeline.unGroupSelection(clipId) } MenuItem { visible: true text: i18n('Copy') onTriggered: timeline.copyClip(trackIndex, index) } MenuSeparator { visible: true } MenuItem { text: i18n('Remove') onTriggered: timeline.triggerAction('delete_timeline_clip') } MenuItem { visible: true text: i18n('Lift') onTriggered: timeline.lift(trackIndex, index) } MenuSeparator { visible: true } MenuItem { visible: true text: i18n('Split At Playhead (S)') onTriggered: timeline.splitClip(trackIndex, index) } 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/Composition.qml b/src/timeline2/view/qml/Composition.qml index a1b005f5a..5e186a0a3 100644 --- a/src/timeline2/view/qml/Composition.qml +++ b/src/timeline2/view/qml/Composition.qml @@ -1,386 +1,379 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * Based on work by Dan Dennedy (Shotcut) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ import QtQuick 2.2 import QtQuick.Controls 1.0 import QtGraphicalEffects 1.0 import QtQml.Models 2.2 import QtQuick.Window 2.2 -Rectangle { + +Item { id: compositionRoot property real timeScale: 1.0 property string clipName: '' property string clipResource: '' property string mltService: '' property int modelStart: x + property int displayHeight: 0 property int inPoint: 0 property int outPoint: 0 property int clipDuration: 0 property bool isAudio: false property bool isComposition: true property bool grouped: false property int binId: 0 + property int scrollX: 0 property int trackHeight property int trackIndex //Index in track repeater property int trackId: -42 //Id in the model property int a_track: -1 property int clipId //Id of the clip in the model property int originalTrackId: trackId property int originalX: x property int originalDuration: clipDuration property int lastValidDuration: clipDuration property int draggedX: x property int a_trackPos: -1 property bool selected: false property double speed: 1.0 + property color color: displayRect.color property color borderColor: 'black' x: modelStart * timeScale width : clipDuration * timeScale; signal clicked(var clip, int shiftClick) signal moved(var clip) signal dragged(var clip, var mouse) signal dropped(var clip) signal draggedToTrack(var clip, int pos) signal trimmingIn(var clip, real newDuration, var mouse) signal trimmedIn(var clip) signal trimmingOut(var clip, real newDuration, var mouse) signal trimmedOut(var clip) onTrackHeightChanged: { a_trackPos = root.getTrackYFromId(a_track) - mapToItem(trackRoot, 0, 0).y - trackRoot.mapToItem(null, 0, 0).y + ruler.height } onClipDurationChanged: { width = clipDuration * timeScale; } onTimeScaleChanged: { width = clipDuration * timeScale; + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } - - SystemPalette { id: activePalette } - color: Qt.darker(getColor()) - - border.color: selected? 'red' : borderColor - border.width: 1.5 - clip: false - Drag.active: mouseArea.drag.active - Drag.proposedAction: Qt.MoveAction - opacity: Drag.active? 0.5 : 1.0 - - function getColor() { - if (mltService === 'color') { - //console.log('clip color', clipResource, " / ", '#' + clipResource.substring(2, 8)) - if (clipResource.length == 10) { - // 0xRRGGBBAA - return '#' + clipResource.substring(2, 8) - } - } - return 'mediumpurple' - //root.shotcutBlue + onScrollXChanged: { + labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } - function reparent(track) { parent = track isAudio = track.isAudio - height = track.height / 2 + displayHeight = track.height / 2 y = track.y + track.height / 2 - generateWaveform() + } + onA_trackChanged: { + a_trackPos = root.getTrackYFromId(a_track) - mapToItem(trackRoot, 0, 0).y - trackRoot.mapToItem(null, 0, 0).y + ruler.height } - + SystemPalette { id: activePalette } Rectangle { - // text background - color: 'lightgray' - opacity: 0.7 + id: displayRect anchors.top: parent.top + anchors.right: parent.right anchors.left: parent.left - anchors.topMargin: parent.border.width - anchors.leftMargin: parent.border.width - // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) - width: Math.min(label.width + 2, parent.width - 2) - height: label.height - } - - Text { - id: label - text: clipName - font.pixelSize: root.baseUnit - width: parent.width - 2 + height: displayHeight + color: Qt.darker('mediumpurple') + border.color: selected? 'red' : borderColor + border.width: 1.5 + opacity: Drag.active? 0.5 : 1.0 clip: true - anchors { - top: parent.top - left: parent.left - topMargin: parent.border.width + 1 - leftMargin: parent.border.width + 1 - // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 - } - color: 'black' - } - // target track - Rectangle { - width: parent.width - height: 8 - gradient: Gradient { - GradientStop { position: 0.0; color: "mediumpurple" } - GradientStop { position: 1.0; color: "#00000000" } + Rectangle { + // text background + id: labelRect + color: 'lightgray' + opacity: 0.7 + anchors.top: parent.top + anchors.topMargin: parent.border.width + anchors.leftMargin: parent.border.width + // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + width: label.width + 2 + height: label.height + Text { + id: label + text: clipName + font.pixelSize: root.baseUnit + anchors { + top: parent.top + left: parent.left + topMargin: parent.border.width + 1 + leftMargin: parent.border.width + } + color: 'black' + } } - y: a_trackPos - } - - onA_trackChanged: { - a_trackPos = root.getTrackYFromId(a_track) - mapToItem(trackRoot, 0, 0).y - trackRoot.mapToItem(null, 0, 0).y + ruler.height - } + Drag.active: mouseArea.drag.active + Drag.proposedAction: Qt.MoveAction states: [ State { name: 'normal' when: !compositionRoot.selected PropertyChanges { target: compositionRoot z: 0 } }, State { name: 'selected' when: compositionRoot.selected PropertyChanges { target: compositionRoot z: 1 - color: getColor() + color: 'mediumpurple' } } ] MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton - drag.target: parent + drag.target: compositionRoot drag.axis: Drag.XAxis property int startX onPressed: { root.stopScrolling = true - originalX = parent.x + originalX = compositionRoot.x originalTrackId = compositionRoot.trackId - startX = parent.x + startX = compositionRoot.x compositionRoot.forceActiveFocus(); compositionRoot.clicked(compositionRoot, mouse.modifiers === Qt.ShiftModifier) } onPositionChanged: { - if (mouse.y < 0 || mouse.y > height) { - parent.draggedToTrack(compositionRoot, mapToItem(null, 0, mouse.y).y) + if (mouse.y < -compositionRoot.parent.height / 2 || mouse.y > height) { + compositionRoot.draggedToTrack(compositionRoot, mapToItem(null, 0, mouse.y).y) } else { - parent.dragged(compositionRoot, mouse) + compositionRoot.dragged(compositionRoot, mouse) } } onReleased: { root.stopScrolling = false - parent.y = compositionRoot.parent.height / 2 - var delta = parent.x - startX + compositionRoot.y = compositionRoot.parent.height / 2 + var delta = compositionRoot.x - startX if (Math.abs(delta) >= 1.0 || trackId !== originalTrackId) { - parent.moved(compositionRoot) - originalX = parent.x + compositionRoot.moved(compositionRoot) + originalX = compositionRoot.x originalTrackId = trackId } else { - parent.dropped(compositionRoot) + compositionRoot.dropped(compositionRoot) } } onDoubleClicked: timeline.position = compositionRoot.x / timeline.scaleFactor onWheel: zoomByWheel(wheel) MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton propagateComposedEvents: true cursorShape: (trimInMouseArea.drag.active || trimOutMouseArea.drag.active)? Qt.SizeHorCursor : drag.active? Qt.ClosedHandCursor : Qt.OpenHandCursor onPressed: { root.stopScrolling = true compositionRoot.forceActiveFocus(); if (!compositionRoot.selected) { compositionRoot.clicked(compositionRoot, false) } } onClicked: menu.show() } } Rectangle { id: trimIn anchors.left: parent.left anchors.leftMargin: 0 height: parent.height width: 5 color: isAudio? 'green' : 'lawngreen' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis onPressed: { root.stopScrolling = true compositionRoot.originalX = mapToItem(null, x, y).x compositionRoot.originalDuration = clipDuration parent.anchors.left = undefined } onReleased: { root.stopScrolling = false parent.anchors.left = compositionRoot.left compositionRoot.trimmedIn(compositionRoot) parent.opacity = 0 } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { compositionRoot.draggedX = mapToItem(null, x, y).x var delta = Math.round((draggedX - originalX) / timeScale) if (delta !== 0) { var newDuration = compositionRoot.clipDuration - delta compositionRoot.trimmingIn(compositionRoot, newDuration, mouse) } } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } Rectangle { id: trimOut anchors.right: parent.right anchors.rightMargin: 0 height: parent.height width: 5 color: 'red' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis onPressed: { root.stopScrolling = true compositionRoot.originalDuration = clipDuration parent.anchors.right = undefined } onReleased: { root.stopScrolling = false parent.anchors.right = compositionRoot.right compositionRoot.trimmedOut(compositionRoot) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var newDuration = Math.round((parent.x + parent.width) / timeScale) compositionRoot.trimmingOut(compositionRoot, newDuration, mouse) } } onEntered: parent.opacity = 0.5 onExited: parent.opacity = 0 } } +} + // target track + Rectangle { + width: parent.width + height: 8 + gradient: Gradient { + GradientStop { position: 0.0; color: selected ? 'red' : 'mediumpurple' } + GradientStop { position: 1.0; color: "#00000000" } + } + y: a_trackPos + } Menu { id: menu function show() { //mergeItem.visible = timeline.mergeClipWithNext(trackIndex, index, true) popup() } MenuItem { text: i18n('Cut') onTriggered: { if (!trackRoot.isLocked) { timeline.copyClip(trackIndex, index) timeline.remove(trackIndex, index) } else { root.pulseLockButtonOnTrack(currentTrack) } } } MenuItem { visible: !grouped && trackRoot.selection.length > 1 text: i18n('Group') onTriggered: timeline.groupSelection() } MenuItem { visible: grouped text: i18n('Ungroup') onTriggered: timeline.unGroupSelection(clipId) } MenuItem { visible: true text: i18n('Copy') onTriggered: timeline.copyClip(trackIndex, index) } MenuSeparator { visible: !isComposition } MenuItem { text: i18n('Remove') onTriggered: timeline.triggerAction('delete_timeline_clip') } MenuItem { visible: true text: i18n('Lift') onTriggered: timeline.lift(trackIndex, index) } MenuSeparator { visible: true } MenuItem { visible: true text: i18n('Split At Playhead (S)') onTriggered: timeline.splitClip(trackIndex, index) } MenuItem { id: mergeItem text: i18n('Merge with next clip') onTriggered: timeline.mergeClipWithNext(trackIndex, index, false) } MenuItem { visible: !isComposition 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/Track.qml b/src/timeline2/view/qml/Track.qml index b17d57475..5f00b3f28 100644 --- a/src/timeline2/view/qml/Track.qml +++ b/src/timeline2/view/qml/Track.qml @@ -1,355 +1,361 @@ /* * 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.0 import QtQml.Models 2.1 Column{ id: trackRoot property alias model: trackModel.model property alias rootIndex: trackModel.rootIndex property bool isAudio property real timeScale: 1.0 property bool isCurrentTrack: false property bool isLocked: false property var selection property int trackId : -42 height: parent.height SystemPalette { id: activePalette } signal clipClicked(var clip, var track, int shiftClick) signal clipDragged(var clip, int x, int y) signal clipDropped(var clip) signal compositionDropped(var clip) signal clipDraggedToTrack(var clip, int pos) signal compositionDraggedToTrack(var composition, int pos) function redrawWaveforms() { for (var i = 0; i < repeater.count; i++) repeater.itemAt(i).generateWaveform() } function clipAt(index) { return repeater.itemAt(index) } width: clipRow.width DelegateModel { id: trackModel delegate: Item{ property var itemModel : model Loader { id: loader Binding { target: loader.item property: "timeScale" value: trackRoot.timeScale when: loader.status == Loader.Ready } Binding { target: loader.item property: "selected" value: trackRoot.selection.indexOf(loader.item.clipId) !== -1 when: loader.status == Loader.Ready } Binding { target: loader.item property: "mltService" value: model.mlt_service when: loader.status == Loader.Ready } Binding { target: loader.item property: "modelStart" value: model.start when: loader.status == Loader.Ready } + Binding { + target: loader.item + property: "scrollX" + value: scrollView.flickableItem.contentX + when: loader.status == Loader.Ready + } Binding { target: loader.item property: "a_track" value: model.a_track when: loader.status == Loader.Ready && loader.item.isComposition } Binding { target: loader.item property: "trackHeight" value: root.trackHeight when: loader.status == Loader.Ready && loader.item.isComposition } Binding { target: loader.item property: "clipDuration" value: model.duration when: loader.status == Loader.Ready } Binding { target: loader.item property: "inPoint" value: model.in when: loader.status == Loader.Ready } Binding { target: loader.item property: "outPoint" value: model.out when: loader.status == Loader.Ready } sourceComponent: { if (model.isComposition) { return compositionDelegate } else { return clipDelegate } } onLoaded: { console.log('loaded clip: ', model.start) item.clipName= model.name item.clipResource= model.resource item.clipId= model.item item.binId= model.binId item.isAudio= false //model.audio item.isComposition= model.isComposition if (!model.isComposition) { item.audioLevels= model.audioLevels item.markers= model.markers item.fadeIn= 0 //model.fadeIn item.fadeOut= 0 //model.fadeOut } else { item.a_track = model.a_track } item.grouped= model.grouped item.borderColor= (model.grouped ? 'yellow' : 'black') item.trackIndex= trackRoot.DelegateModel.itemsIndex item.trackId= trackRoot.trackId //hash= model.hash item.speed= 1 //model.speed item.selected= trackRoot.selection.indexOf(item.clipId) !== -1 console.log(width, height); } } } } Item { id: clipRow height: trackRoot.height Repeater { id: repeater; model: trackModel } } Component { id: clipDelegate Clip { z: 10 height: trackRoot.height onGroupedChanged: { console.log('Clip ', clipId, ' is grouped : ', grouped) flashclip.start() } SequentialAnimation on color { id: flashclip loops: 2 running: false ColorAnimation { from: Qt.darker(getColor()); to: "#ff3300"; duration: 100 } ColorAnimation { from: "#ff3300"; to: Qt.darker(getColor()); duration: 100 } } onClicked: { console.log("Clip clicked",clip.clipId) trackRoot.clipClicked(clip, trackRoot, shiftClick); clip.draggedX = clip.x } onMoved: { //called when the movement is finished var toTrack = clip.trackId var cIndex = clip.clipId var frame = Math.round(clip.x / timeScale) var origFrame = Math.round(clip.originalX / timeScale) console.log("Asking move ",toTrack, cIndex, frame) controller.requestClipMove(cIndex, clip.originalTrackId, origFrame, false, false) var val = controller.requestClipMove(cIndex, toTrack, frame, true, true) console.log("RESULT", val) } onDragged: { //called when the move is in process var toTrack = clip.trackId var cIndex = clip.clipId clip.x = Math.max(0, clip.x) var frame = Math.round(clip.x / timeScale) frame = controller.suggestClipMove(cIndex, toTrack, frame); if (!controller.requestClipMove(cIndex, toTrack, frame, false, false)) { // Abort move clip.x = clip.draggedX } else { clip.x = frame * timeScale } var mapped = trackRoot.mapFromItem(clip, mouse.x, mouse.y) trackRoot.clipDragged(clip, mapped.x, mapped.y) clip.draggedX = clip.x } onTrimmingIn: { if (controller.requestItemResize(clip.clipId, newDuration, false, false, true)) { clip.lastValidDuration = newDuration clip.originalX = clip.draggedX // Show amount trimmed as a time in a "bubble" help. var delta = newDuration - clip.originalDuration var s = timeline.timecode(Math.abs(delta)) s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '') .arg(s.substring(3)) .arg(timeline.timecode(clipDuration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedIn: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, true) controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, true) } onTrimmingOut: { if (controller.requestItemResize(clip.clipId, newDuration, true, false, true)) { clip.lastValidDuration = newDuration // Show amount trimmed as a time in a "bubble" help. var delta = newDuration - clip.originalDuration var s = timeline.timecode(Math.abs(delta)) s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '') .arg(s.substring(3)) .arg(timeline.timecode(clipDuration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedOut: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, true) controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, true) } Component.onCompleted: { moved.connect(trackRoot.clipDropped) dropped.connect(trackRoot.clipDropped) draggedToTrack.connect(trackRoot.clipDraggedToTrack) console.log('Showing CLIP item ', model.clipId, 'name', model.name, ' service: ',mltService) } } } Component { id: compositionDelegate Composition { - z: 20 y: trackRoot.height / 2 - height: trackRoot.height / 2 + displayHeight: trackRoot.height / 2 opacity: 0.6 selected: trackRoot.selection.indexOf(clipId) !== -1 onGroupedChanged: { console.log('Composition ', clipId, ' is grouped : ', grouped) flashclip.start() } SequentialAnimation on color { id: flashclip loops: 2 running: false - ColorAnimation { from: Qt.darker(getColor()); to: "#ff3300"; duration: 100 } - ColorAnimation { from: "#ff3300"; to: Qt.darker(getColor()); duration: 100 } + ColorAnimation { from: Qt.darker('mediumpurple'); to: "#ff3300"; duration: 100 } + ColorAnimation { from: "#ff3300"; to: Qt.darker('mediumpurple'); duration: 100 } } onClicked: { console.log("Composition clicked",clip.clipId) trackRoot.clipClicked(clip, trackRoot, shiftClick); clip.draggedX = clip.x } onMoved: { //called when the movement is finished console.log("Composition released",clip.clipId) var toTrack = clip.trackId var cIndex = clip.clipId var frame = Math.round(clip.x / timeScale) var origFrame = Math.round(clip.originalX / timeScale) console.log("Asking move ",toTrack, cIndex, frame) controller.requestCompositionMove(cIndex, clip.originalTrackId, origFrame, false, false) var val = controller.requestCompositionMove(cIndex, toTrack, frame, true, true) console.log("RESULT", val) } onDragged: { //called when the move is in process var toTrack = clip.trackId var cIndex = clip.clipId clip.x = Math.max(0, clip.x) var frame = Math.round(clip.x / timeScale) + console.log('compoit drag: ', frame) frame = controller.suggestCompositionMove(cIndex, toTrack, frame); if (!controller.requestCompositionMove(cIndex, toTrack, frame, false, false)) { // Abort move clip.x = clip.draggedX } else { clip.x = frame * timeScale } var mapped = trackRoot.mapFromItem(clip, mouse.x, mouse.y) trackRoot.clipDragged(clip, mapped.x, mapped.y) clip.draggedX = clip.x } onTrimmingIn: { if (controller.requestItemResize(clip.clipId, newDuration, false, false, true)) { clip.lastValidDuration = newDuration clip.originalX = clip.draggedX // Show amount trimmed as a time in a "bubble" help. var delta = newDuration - clip.originalDuration var s = timeline.timecode(Math.abs(delta)) s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '') .arg(s.substring(3)) .arg(timeline.timecode(clipDuration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedIn: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, true) controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, true) } onTrimmingOut: { if (controller.requestItemResize(clip.clipId, newDuration, true, false, true)) { clip.lastValidDuration = newDuration // Show amount trimmed as a time in a "bubble" help. var delta = newDuration - clip.originalDuration var s = timeline.timecode(Math.abs(delta)) s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '') .arg(s.substring(3)) .arg(timeline.timecode(clipDuration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedOut: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, true) controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, true) } Component.onCompleted: { moved.connect(trackRoot.compositionDropped) dropped.connect(trackRoot.compositionDropped) draggedToTrack.connect(trackRoot.compositionDraggedToTrack) // console.log('Showing item ', model.item, 'name', model.name, ' service: ',mltService) } } } } diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml index 58fab90f7..c85942e57 100644 --- a/src/timeline2/view/qml/timeline.qml +++ b/src/timeline2/view/qml/timeline.qml @@ -1,627 +1,627 @@ import QtQuick 2.4 import QtQml.Models 2.1 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 import Kdenlive.Controls 1.0 import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 import 'Timeline.js' as Logic Rectangle { id: root objectName: "timelineview" SystemPalette { id: activePalette } color: activePalette.window signal clipClicked() FontMetrics { id: fontMetrics font.family: "Arial" } function zoomByWheel(wheel) { if (wheel.modifiers & Qt.ControlModifier) { //TODO timeline.setScaleFactor(timeline.scaleFactor + 0.2 * wheel.angleDelta.y / 120); } else { scrollView.flickableItem.contentX = Math.max(0, scrollView.flickableItem.contentX + wheel.angleDelta.y) } //Logic.scrollIfNeeded() } function continuousScrolling(x) { // This provides continuous scrolling at the left/right edges. if (x > scrollView.flickableItem.contentX + scrollView.width - 50) { scrollTimer.item = clip scrollTimer.backwards = false scrollTimer.start() } else if (x < 50) { scrollView.flickableItem.contentX = 0; scrollTimer.stop() } else if (x < scrollView.flickableItem.contentX + 50) { scrollTimer.item = clip scrollTimer.backwards = true scrollTimer.start() } else { scrollTimer.stop() } } function getTrackYFromId(a_track) { return Logic.getTrackYFromId(a_track) } function getTrackColor(audio, header) { var col = activePalette.alternateBase if (audio) { col = Qt.tint(col, "#1000cc00") } if (header) { col = Qt.darker(col, 1.05) } return col } property int headerWidth: 140 property real baseUnit: fontMetrics.font.pointSize property int currentTrack: 0 property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4) property alias trackCount: tracksRepeater.count property bool stopScrolling: false property int seekPos: 0 property int duration: timeline.duration property color shotcutBlue: Qt.rgba(23/255, 92/255, 118/255, 1.0) property int clipBeingDroppedId: -1 property string clipBeingDroppedData property int droppedPosition: -1 property int droppedTrack: -1 property int clipBeingMovedId: -1 property real timeScale: timeline.scaleFactor property int trackHeight //property alias ripple: toolbar.ripple //onCurrentTrackChanged: timeline.selection = [] onTimeScaleChanged: { scrollView.flickableItem.contentX = Math.max(0, root.seekPos * timeline.scaleFactor - (scrollView.width / 2)) ruler.adjustStepSize() } DropArea { //Drop area for compositions width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/composition' onEntered: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame if (track >= 0) { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) drag.acceptProposedAction() } else { drag.accepted = false } } } onPositionChanged: { console.log('======================== ON POS CHANGED ========================================') if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) if (clipBeingDroppedId >= 0){ controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { clipBeingDroppedData = drag.getDataAsString('kdenlive/composition') clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestCompositionDeletion(clipBeingDroppedId, false) } clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getCompositionPosition(clipBeingDroppedId) var track = controller.getCompositionTrackId(clipBeingDroppedId) // we simulate insertion at the final position so that stored undo has correct value controller.requestItemDeletion(clipBeingDroppedId, false) timeline.insertComposition(track, frame, clipBeingDroppedData, true) } clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false } } DropArea { //Drop area for bin/clips width: root.width - headerWidth height: root.height - ruler.height y: ruler.height x: headerWidth keys: 'kdenlive/producerslist' onEntered: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame if (track >= 0) { //drag.acceptProposedAction() clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist') clipBeingDroppedId = timeline.insertClip(track, frame, clipBeingDroppedData, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { drag.accepted = false } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestClipDeletion(clipBeingDroppedId, false) } clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false } onPositionChanged: { console.log('======================== ON POS CHANGED ========================================') if (clipBeingMovedId == -1) { var track = Logic.getTrackIdFromPos(drag.y) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) if (clipBeingDroppedId >= 0){ controller.requestClipMove(clipBeingDroppedId, track, frame, true, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { clipBeingDroppedId = timeline.insertClip(track, frame, drag.getDataAsString('kdenlive/producerslist'), false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getClipPosition(clipBeingDroppedId) var track = controller.getClipTrackId(clipBeingDroppedId) // we simulate insertion at the final position so that stored undo has correct value controller.requestItemDeletion(clipBeingDroppedId, false) timeline.insertClip(track, frame, clipBeingDroppedData, true) } clipBeingDroppedId = -1 droppedPosition = -1 droppedTrack = -1 scrollTimer.running = false } } Menu { id: menu MenuItem { text: i18n('Add Audio Track') shortcut: 'Ctrl+U' onTriggered: timeline.addAudioTrack(); } } Menu { id: headerMenu MenuItem { text: i18n('Add Track') shortcut: 'Ctrl+U' onTriggered: timeline.addTrack(currentTrack); } MenuItem { text: i18n('Delete Track') //shortcut: 'Ctrl+U' onTriggered: timeline.deleteTrack(currentTrack); } } Row { Column { z: 1 Rectangle { id: cornerstone property bool selected: false // Padding between toolbar and track headers. width: headerWidth height: ruler.height color: selected? shotcutBlue : activePalette.window border.color: selected? 'red' : 'transparent' border.width: selected? 1 : 0 z: 1 MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton onClicked: { timeline.selectMultitrack() } } } Flickable { // Non-slider scroll area for the track headers. contentY: scrollView.flickableItem.contentY width: headerWidth height: trackHeaders.height interactive: false Column { id: trackHeaders spacing: 0 Repeater { id: trackHeaderRepeater model: multitrack TrackHead { trackName: model.name isMute: model.mute isHidden: model.hidden isComposite: model.composite isLocked: model.locked isAudio: model.audio width: headerWidth height: model.trackHeight selected: false current: index === currentTrack trackId: item onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked onMyTrackHeightChanged: { model.trackHeight = myTrackHeight trackBaseRepeater.itemAt(index).height = myTrackHeight tracksRepeater.itemAt(index).height = myTrackHeight height = myTrackHeight // hack: change property to trigger transition adjustment root.trackHeight = root.trackHeight === 1 ? 0 : 1 } onClicked: { currentTrack = index console.log('track name: ',index, ' = ', model.name) //timeline.selectTrackHead(currentTrack) } } } } } } MouseArea { id: tracksArea width: root.width - headerWidth height: root.height // This provides continuous scrubbing and scimming at the left/right edges. hoverEnabled: true acceptedButtons: Qt.RightButton | Qt.LeftButton onWheel: { root.seekPos += (wheel.angleDelta.y > 0 ? 1 : -1) timeline.position = root.seekPos } onClicked: { if (mouse.button & Qt.RightButton) { menu.popup() } else { console.log("Position changed: ",timeline.position) root.seekPos = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor timeline.position = root.seekPos } } property bool scim: false onReleased: scim = false onExited: scim = false onPositionChanged: { if (/*mouse.modifiers === Qt.ShiftModifier ||*/ mouse.buttons === Qt.LeftButton) { root.seekPos = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor timeline.position = root.seekPos scim = true } else scim = false } Timer { id: scrubTimer interval: 25 repeat: true running: parent.scim && parent.containsMouse && (parent.mouseX < 50 || parent.mouseX > parent.width - 50) && (timeline.position * timeline.scaleFactor >= 50) onTriggered: { if (parent.mouseX < 50) root.seekPos = timeline.position - 10 else root.seekPos = timeline.position + 10 timeline.position = root.seekPos } } Column { Flickable { // Non-slider scroll area for the Ruler. contentX: scrollView.flickableItem.contentX width: root.width - headerWidth height: ruler.height interactive: false Ruler { id: ruler width: root.duration index: index } } ScrollView { id: scrollView width: root.width - headerWidth height: root.height - ruler.height // Click and drag should seek, not scroll the timeline view flickableItem.interactive: false Rectangle { width: root.duration height: trackHeaders.height color: activePalette.window MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: zoomByWheel(wheel) } Column { // These make the striped background for the tracks. // It is important that these are not part of the track visual hierarchy; // otherwise, the clips will be obscured by the Track's background. Repeater { model: multitrack id: trackBaseRepeater delegate: Rectangle { width: root.duration border.width: 1 border.color: Qt.rgba(activePalette.windowText.r, activePalette.windowText.g, activePalette.windowText.b, 0.1) //Layout.fillWidth: true height: model.trackHeight color: tracksRepeater.itemAt(index) ? ((index === currentTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red' } } } Column { id: tracksContainer Repeater { id: tracksRepeater; model: trackDelegateModel } } } } } /*CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0 } CornerSelectionShadow { y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0 clip: timeline.selection.length ? tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0 anchors.right: parent.right mirrorGradient: true }*/ Rectangle { id: cursor visible: timeline.position > -1 color: activePalette.text width: Math.max(1, 1 * timeline.scaleFactor) opacity: (width > 2) ? 0.5 : 1 height: root.height - scrollView.__horizontalScrollBar.height - ruler.height x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX y: ruler.height } Rectangle { id: seekCursor visible: timeline.position != root.seekPos color: activePalette.highlight width: 4 height: ruler.height opacity: 0.5 x: root.seekPos * timeline.scaleFactor - scrollView.flickableItem.contentX y: 0 } TimelinePlayhead { id: playhead visible: timeline.position > -1 height: baseUnit width: baseUnit * 1.5 y: ruler.height - height x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX - (width / 2) } } } Rectangle { id: bubbleHelp property alias text: bubbleHelpLabel.text color: activePalette.window //application.toolTipBaseColor width: bubbleHelpLabel.width + 8 height: bubbleHelpLabel.height + 8 radius: 4 states: [ State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} }, State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} } ] state: 'invisible' transitions: [ Transition { from: 'invisible' to: 'visible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } }, Transition { from: 'visible' to: 'invisible' OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad } } ] Label { id: bubbleHelpLabel color: activePalette.text //application.toolTipTextColor anchors.centerIn: parent font.pixelSize: root.baseUnit } function show(x, y, text) { bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height bubbleHelp.text = text if (bubbleHelp.state !== 'visible') bubbleHelp.state = 'visible' } function hide() { bubbleHelp.state = 'invisible' bubbleHelp.opacity = 0 } } /*DropShadow { source: bubbleHelp anchors.fill: bubbleHelp opacity: bubbleHelp.opacity horizontalOffset: 3 verticalOffset: 3 radius: 8 color: '#80000000' transparentBorder: true fast: true }*/ DelegateModel { id: trackDelegateModel model: multitrack Track { model: multitrack rootIndex: trackDelegateModel.modelIndex(index) height: trackHeight timeScale: timeline.scaleFactor width: root.duration * timeScale isAudio: audio isCurrentTrack: currentTrack === index trackId: item selection: timeline.selection onClipClicked: { currentTrack = track.DelegateModel.itemsIndex if (shiftClick === 1) { timeline.addSelection(clip.clipId) } else { timeline.selection = [ clip.clipId ] } //root.clipClicked() } onClipDragged: { continuousScrolling(x) // Show distance moved as time in a "bubble" help. var track = tracksRepeater.itemAt(clip.trackIndex) var delta = Math.round((clip.x - clip.originalX) / timeline.scaleFactor) var s = timeline.timecode(Math.abs(delta)) // remove leading zeroes if (s.substring(0, 3) === '00:') s = s.substring(3) s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s bubbleHelp.show(x, track.y + height/2, s) clipBeingMovedId = clip.clipId } onClipDropped: { console.log(" + + + ++ + DROPPED + + + + + + +"); scrollTimer.running = false bubbleHelp.hide() clipBeingMovedId = -1 } onCompositionDropped: { console.log(" + + + ++ + COMPOSITION DROPPED + + + + + + +"); scrollTimer.running = false bubbleHelp.hide() clipBeingMovedId = -1 var track = tracksRepeater.itemAt(clip.trackIndex) clip.y = track.height / 2 } onClipDraggedToTrack: { var y = pos - ruler.height currentTrack = Logic.getTrackIndexFromPos(y) var frame = Math.round(clip.x / timeScale) if (currentTrack >= 0 && currentTrack < tracksRepeater.count) { var track = tracksRepeater.itemAt(currentTrack) if (controller.requestClipMove(clip.clipId, track.trackId, frame, false, false)) { clip.reparent(track) clip.trackIndex = track.DelegateModel.itemsIndex clip.trackId = track.trackId } } } onCompositionDraggedToTrack: { - console.log('Composition Dragged to Track') + console.log('Composition Dragged to Track: ', pos) var y = pos - ruler.height currentTrack = Logic.getTrackIndexFromPos(y) var frame = Math.round(composition.x / timeScale) if (currentTrack >= 0 && currentTrack < tracksRepeater.count) { var track = tracksRepeater.itemAt(currentTrack) if (controller.requestCompositionMove(composition.clipId, track.trackId, frame, false, false)) { composition.reparent(track) composition.trackIndex = track.DelegateModel.itemsIndex composition.trackId = track.trackId } } } Image { anchors.right: parent.right anchors.left: parent.left height: parent.height source: "qrc:///pics/kdenlive-lock.svgz" fillMode: Image.Tile opacity: parent.isLocked visible: opacity Behavior on opacity { NumberAnimation {} } MouseArea { anchors.fill: parent onPressed: { mouse.accepted = true; trackHeaderRepeater.itemAt(index).pulseLockButton() } } } } } Connections { target: timeline onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded() /*onDragging: Logic.dragging(pos, duration) onDropped: Logic.dropped() onDropAccepted: Logic.acceptDrop(xml)*/ onSelectionChanged: { cornerstone.selected = timeline.isMultitrackSelected() var selectedTrack = timeline.selectedTrack() for (var i = 0; i < trackHeaderRepeater.count; i++) trackHeaderRepeater.itemAt(i).selected = (i === selectedTrack) } } // This provides continuous scrolling at the left/right edges. Timer { id: scrollTimer interval: 25 repeat: true triggeredOnStart: true property var item property bool backwards onTriggered: { var delta = backwards? -10 : 10 if (item) item.x += delta scrollView.flickableItem.contentX += delta if (scrollView.flickableItem.contentX <= 0) stop() } } }