diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml index d9b6f22e4..5a8519b35 100644 --- a/src/timeline2/view/qml/Clip.qml +++ b/src/timeline2/view/qml/Clip.qml @@ -1,962 +1,960 @@ /* * 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 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: false 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 > (clipDuration * timeline.scaleFactor) || scrollStart + scrollView.viewport.width < 0 } onIsGrabbedChanged: { if (clipRoot.isGrabbed) { grabItem() } else { mouseArea.focus = false } } function grabItem() { clipRoot.forceActiveFocus() mouseArea.focus = true } function clearAndMove(offset) { controller.requestClearSelection() controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - offset, true, true, true) controller.requestAddToSelection(clipRoot.clipId) } onClipResourceChanged: { if (itemType == ProducerType.Color) { color: Qt.darker(getColor(), 1.5) } } ToolTip { visible: mouseArea.containsMouse && !dragProxyArea.pressed delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text font.pointSize: root.fontUnit text: label.text + ' (' + timeline.simplifiedTC(clipRoot.inPoint) + '-' + timeline.simplifiedTC(clipRoot.outPoint) + ')' } } onKeyframeModelChanged: { if (effectRow.keyframecanvas) { console.log('keyframe model changed............') effectRow.keyframecanvas.requestPaint() } } onClipDurationChanged: { width = clipDuration * timeScale; if (parentTrack && parentTrack.isAudio && thumbsLoader.item) { // Duration changed, we may need a different number of repeaters thumbsLoader.item.reload() } } onModelStartChanged: { x = modelStart * timeScale; } onFakePositionChanged: { x = fakePosition * timeScale; } onFakeTidChanged: { if (clipRoot.fakeTid > -1 && parentTrack) { if (clipRoot.parent != dragContainer) { var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y); clipRoot.parent = dragContainer pos = clipRoot.mapFromGlobal(pos.x, pos.y) clipRoot.x = pos.x clipRoot.y = pos.y } clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y } } onForceReloadThumbChanged: { // TODO: find a way to force reload of clip thumbs if (thumbsLoader.item) { thumbsLoader.item.reload() } } onTimeScaleChanged: { x = modelStart * timeScale; width = clipDuration * timeScale; labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } onScrollXChanged: { labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0 } border.color: selected ? root.selectionColor : grouped ? root.groupColor : borderColor border.width: isGrabbed ? 8 : 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 noThumbs: (isAudio || itemType == ProducerType.Color || mltService === '') property bool isImage: itemType == ProducerType.Image - property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + documentId + '/' + (isImage ? '#0' : '#') + property string baseThumbPath: noThumbs ? '' : 'image://thumbnail/' + binId + '/' + documentId + '/' + (isImage ? '#0' : '#') + property string baseCacheThumbPath: noThumbs ? '' : 'image://thumbnailCache/' + binId + '/' + (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 { + Loader { // Thumbs container + id: thumbsLoader 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" : "" - } + asynchronous: true + visible: status == Loader.Ready + source: clipRoot.hideClipViews ? "" : parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : "" } Item { // Clipping container id: container anchors.fill: parent anchors.margins: 1.5 clip: true Rectangle { // text background id: labelRect color: clipRoot.selected ? 'darkred' : '#66000000' width: label.width + 2 height: label.height visible: clipRoot.width > width / 2 Text { id: label text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '') font.pointSize: root.fontUnit anchors { top: labelRect.top left: labelRect.left topMargin: 1 leftMargin: 1 } color: 'white' style: Text.Outline styleColor: 'black' } } Rectangle { // Offset info id: offsetRect color: 'darkgreen' width: offsetLabel.width + radius height: offsetLabel.height radius: height/3 x: labelRect.width + 4 visible: labelRect.visible && positionOffset != 0 MouseArea { id: offsetArea hoverEnabled: true cursorShape: Qt.PointingHandCursor anchors.fill: parent onClicked: { clearAndMove(positionOffset) } ToolTip { visible: offsetArea.containsMouse delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text font.pointSize: root.fontUnit text: positionOffset < 0 ? i18n("Offset: -%1", timeline.simplifiedTC(-positionOffset)) : i18n("Offset: %1", timeline.simplifiedTC(positionOffset)) } } Text { id: offsetLabel text: positionOffset font.pointSize: root.fontUnit anchors { horizontalCenter: parent.horizontalCenter topMargin: 1 leftMargin: 1 } color: 'white' style: Text.Outline styleColor: 'black' } } } Rectangle { // effects id: effectsRect color: '#555555' width: effectLabel.width + 2 height: effectLabel.height x: labelRect.x anchors.top: labelRect.bottom visible: labelRect.visible && clipRoot.effectNames != '' Text { id: effectLabel text: clipRoot.effectNames font.pointSize: root.fontUnit anchors { top: effectsRect.top left: effectsRect.left topMargin: 1 leftMargin: 1 // + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1 } color: 'white' //style: Text.Outline styleColor: 'black' } } Repeater { model: markers delegate: Item { anchors.fill: parent Rectangle { id: markerBase width: 1 height: parent.height x: clipRoot.speed < 0 ? clipRoot.clipDuration * timeScale + (Math.round(model.frame / clipRoot.speed) - (clipRoot.maxDuration - clipRoot.outPoint)) * timeScale : (Math.round(model.frame / clipRoot.speed) - clipRoot.inPoint) * timeScale; color: model.color } Rectangle { visible: mlabel.visible opacity: 0.7 x: markerBase.x radius: 2 width: mlabel.width + 4 height: mlabel.height anchors { bottom: parent.verticalCenter } color: model.color MouseArea { z: 10 anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true onDoubleClicked: timeline.editMarker(clipRoot.clipId, model.frame) onClicked: proxy.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor } } Text { id: mlabel visible: timeline.showMarkers && parent.width > width * 1.5 text: model.comment font.pointSize: root.fontUnit x: markerBase.x anchors { bottom: parent.verticalCenter topMargin: 2 leftMargin: 2 } color: 'white' } } } KeyframeView { id: effectRow visible: clipRoot.showKeyframes && clipRoot.keyframeModel selected: clipRoot.selected inPoint: clipRoot.inPoint outPoint: clipRoot.outPoint masterObject: clipRoot kfrModel: clipRoot.hideClipViews ? 0 : clipRoot.keyframeModel } } states: [ State { name: 'locked' when: isLocked PropertyChanges { target: clipRoot color: root.lockedColor opacity: 0.8 z: 0 } }, State { name: 'normal' when: clipRoot.selected === false PropertyChanges { target: clipRoot color: Qt.darker(getColor(), 1.5) z: 0 } }, State { name: 'selected' when: clipRoot.selected === true PropertyChanges { target: clipRoot color: getColor() z: 3 } } ] Rectangle { id: compositionIn anchors.left: parent.left anchors.bottom: parent.bottom anchors.bottomMargin: 2 anchors.leftMargin: 4 width: root.baseUnit height: width radius: 2 color: Qt.darker('mediumpurple') border.width: 2 border.color: 'green' opacity: 0 enabled: !clipRoot.isAudio && dragProxy.draggedItem === clipRoot.clipId visible: clipRoot.width > 4 * width MouseArea { id: compInArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: parent.opacity = 0.7 onExited: { if (!pressed) { parent.opacity = 0 } } onPressed: { timeline.addCompositionToClip('', clipRoot.clipId, 0) } onReleased: { parent.opacity = 0 } ToolTip { visible: compInArea.containsMouse && !dragProxyArea.pressed delay: 1000 timeout: 5000 background: Rectangle { color: activePalette.alternateBase border.color: activePalette.light } contentItem: Label { color: activePalette.text font.pointSize: root.fontUnit text: i18n("Click to add composition") } } } } Rectangle { id: compositionOut anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: 2 anchors.rightMargin: 4 width: root.baseUnit 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 } } 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 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) } } } 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 { id: trimOut anchors.fill: parent anchors.margins: 2 color: 'red' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction visible: trimOutMouseArea.pressed || (root.activeTool === 0 && !mouseArea.drag.active && clipRoot.width > 4 * width) } } MouseArea { id: fadeOutMouseArea anchors.right: fadeOutCanvas.left anchors.rightMargin: -width/2 anchors.top: fadeOutCanvas.top anchors.topMargin: -width / 3 width: root.baseUnit height: width //anchors.fill: parent hoverEnabled: true 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 property int startFadeOut property int lastDuration: -1 onEntered: fadeOutControl.opacity = 0.7 onExited: { if (!pressed) { fadeOutControl.opacity = 0 } } drag.smoothed: false onPressed: { root.autoScrolling = false startFadeOut = clipRoot.fadeOut anchors.right = undefined fadeOutControl.opacity = 1 } onReleased: { fadeOutCanvas.opacity = 0.3 root.autoScrolling = timeline.autoScroll anchors.right = fadeOutCanvas.left if (!fadeOutMouseArea.containsMouse) { fadeOutControl.opacity = 0 } var duration = clipRoot.fadeOut if (duration > 0) { duration += 1 } timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, startFadeOut) bubbleHelp.hide() } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var delta = clipRoot.clipDuration - Math.floor((x + width / 2)/ 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 radius: width / 2 color: '#66FFFFFF' border.width: 2 border.color: 'red' opacity: 0 enabled: !isLocked && !dragProxy.isComposition Drag.active: fadeOutMouseArea.drag.active Rectangle { id: fadeOutMarker anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: parent.width / 3 color: 'red' height: container.height - 1 width: 1 } } } MouseArea { id: fadeInMouseArea anchors.left: fadeInTriangle.right anchors.leftMargin: -width / 2 anchors.top: fadeInTriangle.top anchors.topMargin: -width / 3 width: root.baseUnit height: width hoverEnabled: true cursorShape: Qt.PointingHandCursor drag.target: fadeInMouseArea drag.minimumX: - Math.ceil(width / 2) drag.maximumX: container.width - width / 2 drag.axis: Drag.XAxis drag.smoothed: false property int startFadeIn onEntered: fadeInControl.opacity = 0.7 onExited: { if (!pressed) { fadeInControl.opacity = 0 } } onPressed: { root.autoScrolling = false startFadeIn = clipRoot.fadeIn anchors.left = undefined fadeInControl.opacity = 1 fadeInTriangle.opacity = 0.5 // 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) timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn) bubbleHelp.hide() } 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 radius: width / 2 color: '#FF66FFFF' border.width: 2 border.color: 'green' enabled: !isLocked && !dragProxy.isComposition opacity: 0 visible : clipRoot.width > 3 * width Drag.active: fadeInMouseArea.drag.active Rectangle { id: fadeInMarker anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: parent.width / 3 color: 'green' height: container.height - 1 width: 1 } } } /*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/ClipThumbs.qml b/src/timeline2/view/qml/ClipThumbs.qml index 449c5b024..5031a283f 100644 --- a/src/timeline2/view/qml/ClipThumbs.qml +++ b/src/timeline2/view/qml/ClipThumbs.qml @@ -1,45 +1,84 @@ import QtQuick 2.11 import QtQuick.Controls 2.4 import QtQml.Models 2.11 import com.enums 1.0 Row { id: thumbRow anchors.fill: parent visible: !isAudio opacity: clipStatus == ClipState.Disabled ? 0.2 : 1 property int thumbWidth: container.height * 16.0/9.0 property bool enableCache: clipRoot.itemType == ProducerType.Video || clipRoot.itemType == ProducerType.AV function reload() { console.log('+++++\n\ntriggered ML thumb reload\n\n++++++++++++++') clipRoot.baseThumbPath = clipRoot.variableThumbs ? '' : 'image://thumbnail/' + clipRoot.binId + '/' + Math.random() + '/' + (clipRoot.isImage ? '#0' : '#') } Repeater { id: thumbRepeater // switching the model allows to have different view modes: // 2: will display start / end thumbs // container.width / thumbRow.thumbWidth will display all frames showThumbnails // 1: only show first thumbnail // 0: will disable thumbnails model: parentTrack.trackThumbsFormat == 0 ? 2 : parentTrack.trackThumbsFormat == 1 ? Math.ceil(container.width / thumbRow.thumbWidth) : parentTrack.trackThumbsFormat == 2 ? 1 : 0 property int startFrame: clipRoot.inPoint property int endFrame: clipRoot.outPoint property real imageWidth: Math.max(thumbRow.thumbWidth, container.width / thumbRepeater.count) property int thumbStartFrame: (clipRoot.speed >= 0) ? Math.round(clipRoot.inPoint * clipRoot.speed) : Math.round((clipRoot.maxDuration - clipRoot.inPoint) * -clipRoot.speed - 1) property int thumbEndFrame: (clipRoot.speed >= 0) ? Math.round(clipRoot.outPoint * clipRoot.speed) : Math.round((clipRoot.maxDuration - clipRoot.outPoint) * -clipRoot.speed - 1) Image { width: thumbRepeater.imageWidth height: container.height fillMode: Image.PreserveAspectFit asynchronous: true cache: enableCache - property int currentFrame: Math.floor(clipRoot.inPoint + Math.round((index) * width / timeline.scaleFactor)* clipRoot.speed) + property int currentFrame: thumbRepeater.count < 3 ? (index == 0 ? thumbRepeater.thumbStartFrame : thumbRepeater.thumbEndFrame) : Math.floor(clipRoot.inPoint + Math.round((index) * width / timeline.scaleFactor)* clipRoot.speed) + property int lastFrame: -1 horizontalAlignment: thumbRepeater.count < 3 ? (index == 0 ? Image.AlignLeft : Image.AlignRight) : Image.AlignLeft - source: thumbRepeater.count < 3 ? (index == 0 ? clipRoot.baseThumbPath + thumbRepeater.thumbStartFrame : clipRoot.baseThumbPath + thumbRepeater.thumbEndFrame) : (index * width < clipRoot.scrollStart - width || index * width > clipRoot.scrollStart + scrollView.viewport.width) ? '' : clipRoot.baseThumbPath + currentFrame + source: thumbRepeater.count < 3 ? (clipRoot.baseThumbPath + currentFrame) : (index * width < clipRoot.scrollStart - width || index * width > clipRoot.scrollStart + scrollView.viewport.width) ? '' : clipRoot.baseThumbPath + currentFrame + onStatusChanged: { + if (thumbRepeater.count < 3) { + if (status === Image.Ready) { + lastFrame = currentFrame + } + } + } + BusyIndicator { + running: parent.status != Image.Ready + anchors.left: parent.left + anchors.leftMargin: index < thumbRepeater.count - 1 ? 0 : parent.width - thumbRow.thumbWidth - 1 + implicitWidth: thumbRepeater.imageWidth + implicitHeight: container.height + contentItem: + Image { + id: thumbPlaceholder + visible: parent.running + anchors.fill: parent + horizontalAlignment: Image.AlignLeft + fillMode: Image.PreserveAspectFit + asynchronous: true + } + onRunningChanged: { + if (!running) { + thumbPlaceholder.source = clipRoot.baseCacheThumbPath + parent.lastFrame + console.log('Setting image lastframe: ', parent.lastFrame) + } + } + } + Rectangle { + visible: thumbRepeater.count < 3 + anchors.left: parent.left + anchors.leftMargin: index < thumbRepeater.count - 1 ? thumbRow.thumbWidth : parent.width - thumbRow.thumbWidth - 1 + color: "#ffffff" + opacity: 0.3 + width: 1 + height: parent.height + } } } } diff --git a/src/timeline2/view/qmltypes/thumbnailprovider.cpp b/src/timeline2/view/qmltypes/thumbnailprovider.cpp index 9410a6e35..8d8e39e49 100644 --- a/src/timeline2/view/qmltypes/thumbnailprovider.cpp +++ b/src/timeline2/view/qmltypes/thumbnailprovider.cpp @@ -1,140 +1,125 @@ /* * 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 . */ #include "thumbnailprovider.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "utils/thumbnailcache.hpp" #include #include #include #include #include ThumbnailProvider::ThumbnailProvider() : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) -//, m_profile(pCore->getCurrentProfilePath().toUtf8().constData()) { } ThumbnailProvider::~ThumbnailProvider() = default; QImage ThumbnailProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { QImage result; // id is binID/#frameNumber QString binId = id.section('/', 0, 0); bool ok; int frameNumber = id.section('#', -1).toInt(&ok); if (ok) { if (ThumbnailCache::get()->hasThumbnail(binId, frameNumber, false)) { result = ThumbnailCache::get()->getThumbnail(binId, frameNumber); *size = result.size(); return result; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); if (binClip) { std::shared_ptr prod = binClip->thumbProducer(); if (prod && prod->is_valid()) { result = makeThumbnail(prod, frameNumber, requestedSize); ThumbnailCache::get()->storeThumbnail(binId, frameNumber, result, false); } } - - /*if (m_producers.contains(binId.toInt())) { - producer = m_producers.object(binId.toInt()); - } else { - m_binClip->thumbProducer(); - if (!resource.isEmpty()) { - producer = new Mlt::Producer(m_profile, service.toUtf8().constData(), resource.toUtf8().constData()); - } else { - producer = new Mlt::Producer(m_profile, service.toUtf8().constData()); - } - std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); - if (binClip) { - std::shared_ptr projectProducer = binClip->originalProducer(); - Mlt::Properties original(projectProducer->get_properties()); - Mlt::Properties cloneProps(producer->get_properties()); - cloneProps.pass_list(original, "video_index,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff," - "force_colorspace,set.force_full_luma,templatetext,autorotate,xmldata"); - } - Mlt::Filter scaler(m_profile, "swscale"); - Mlt::Filter padder(m_profile, "resize"); - Mlt::Filter converter(m_profile, "avcolor_space"); - producer->attach(scaler); - producer->attach(padder); - producer->attach(converter); - m_producers.insert(binId.toInt(), producer); - } - if ((producer != nullptr) && producer->is_valid()) { - // result = KThumb::getFrame(producer, frameNumber, 0, 0); - result = makeThumbnail(producer, frameNumber, requestedSize); - ThumbnailCache::get()->storeThumbnail(binId, frameNumber, result, false); - //m_cache->insertImage(key, result); - } else { - qDebug() << "INVALID PRODUCER; " << service << " / " << resource; - }*/ } if (size) *size = result.size(); return result; } QString ThumbnailProvider::cacheKey(Mlt::Properties &properties, const QString &service, const QString &resource, const QString &hash, int frameNumber) { QString time = properties.frames_to_time(frameNumber, mlt_time_clock); // Reduce the precision to centiseconds to increase chance for cache hit // without much loss of accuracy. time = time.left(time.size() - 1); QString key; if (hash.isEmpty()) { key = QString("%1 %2 %3").arg(service).arg(resource).arg(time); QCryptographicHash hash2(QCryptographicHash::Sha1); hash2.addData(key.toUtf8()); key = hash2.result().toHex(); } else { key = QString("%1 %2").arg(hash).arg(time); } return key; } QImage ThumbnailProvider::makeThumbnail(const std::shared_ptr &producer, int frameNumber, const QSize &requestedSize) { Q_UNUSED(requestedSize) producer->seek(frameNumber); QScopedPointer frame(producer->get_frame()); if (frame == nullptr || !frame->is_valid()) { return QImage(); } - int ow = 0; // requestedSize.width(); - int oh = 0; // requestedSize.height(); - /*if (ow > 0 && oh > 0) { - frame->set("rescale.interp", "fastest"); - frame->set("deinterlace_method", "onefield"); - frame->set("top_field_first", -1); - }*/ + int ow = 0; + int oh = 0; mlt_image_format format = mlt_image_rgb24a; const uchar *image = frame->get_image(format, ow, oh); if (image) { QImage temp(ow, oh, QImage::Format_ARGB32); memcpy(temp.scanLine(0), image, (unsigned)(ow * oh * 4)); return temp.rgbSwapped(); } return QImage(); } + +ThumbnailCacheProvider::ThumbnailCacheProvider() + : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) +{ +} + +ThumbnailCacheProvider::~ThumbnailCacheProvider() = default; + +QImage ThumbnailCacheProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + QImage result; + // id is binID/#frameNumber + QString binId = id.section('/', 0, 0); + bool ok; + int frameNumber = id.section('#', -1).toInt(&ok); + if (ok) { + if (ThumbnailCache::get()->hasThumbnail(binId, frameNumber, false)) { + result = ThumbnailCache::get()->getThumbnail(binId, frameNumber); + *size = result.size(); + return result; + } + } + if (size) *size = result.size(); + return result; +} diff --git a/src/timeline2/view/qmltypes/thumbnailprovider.h b/src/timeline2/view/qmltypes/thumbnailprovider.h index 6462f0249..ab9dde0fc 100644 --- a/src/timeline2/view/qmltypes/thumbnailprovider.h +++ b/src/timeline2/view/qmltypes/thumbnailprovider.h @@ -1,42 +1,49 @@ /* * 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 . */ #ifndef THUMBNAILPROVIDER_H #define THUMBNAILPROVIDER_H #include #include #include #include #include #include class ThumbnailProvider : public QQuickImageProvider { public: explicit ThumbnailProvider(); ~ThumbnailProvider() override; QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; private: - QString cacheKey(Mlt::Properties &properties, const QString &service, const QString &resource, const QString &hash, int frameNumber); QImage makeThumbnail(const std::shared_ptr &producer, int frameNumber, const QSize &requestedSize); - QCache m_producers; + QString cacheKey(Mlt::Properties &properties, const QString &service, const QString &resource, const QString &hash, int frameNumber); +}; + +class ThumbnailCacheProvider : public QQuickImageProvider +{ +public: + explicit ThumbnailCacheProvider(); + ~ThumbnailCacheProvider() override; + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; }; #endif // THUMBNAILPROVIDER_H diff --git a/src/timeline2/view/timelinewidget.cpp b/src/timeline2/view/timelinewidget.cpp index 5c07a28f2..949c459b0 100644 --- a/src/timeline2/view/timelinewidget.cpp +++ b/src/timeline2/view/timelinewidget.cpp @@ -1,240 +1,240 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinewidget.h" #include "../model/builders/meltBuilder.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "capture/mediacapture.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/model/effectfilter.hpp" #include "effects/effectlist/model/effecttreemodel.hpp" #include "kdenlivesettings.h" #include "mainwindow.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "monitor/monitorproxy.h" #include "qml/timelineitems.h" #include "qmltypes/thumbnailprovider.h" #include "timelinecontroller.h" #include "transitions/transitionlist/model/transitionfilter.hpp" #include "transitions/transitionlist/model/transitiontreemodel.hpp" #include "utils/clipboardproxy.hpp" #include // #include #include #include #include #include #include #include #include const int TimelineWidget::comboScale[] = {1, 2, 4, 8, 15, 30, 50, 75, 100, 150, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 6000, 15000, 30000}; TimelineWidget::TimelineWidget(QWidget *parent) : QQuickWidget(parent) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); setClearColor(palette().window().color()); registerTimelineItems(); // Build transition model for context menu m_transitionModel = TransitionTreeModel::construct(true, this); m_transitionProxyModel = std::make_unique(this); static_cast(m_transitionProxyModel.get())->setFilterType(true, TransitionType::Favorites); m_transitionProxyModel->setSourceModel(m_transitionModel.get()); m_transitionProxyModel->setSortRole(AssetTreeModel::NameRole); m_transitionProxyModel->sort(0, Qt::AscendingOrder); // Build effects model for context menu m_effectsModel = EffectTreeModel::construct(QStringLiteral(), this); m_effectsProxyModel = std::make_unique(this); static_cast(m_effectsProxyModel.get())->setFilterType(true, EffectType::Favorites); m_effectsProxyModel->setSourceModel(m_effectsModel.get()); m_effectsProxyModel->setSortRole(AssetTreeModel::NameRole); m_effectsProxyModel->sort(0, Qt::AscendingOrder); m_proxy = new TimelineController(this); connect(m_proxy, &TimelineController::zoneMoved, this, &TimelineWidget::zoneMoved); connect(m_proxy, &TimelineController::ungrabHack, this, &TimelineWidget::slotUngrabHack); setResizeMode(QQuickWidget::SizeRootObjectToView); - m_thumbnailer = new ThumbnailProvider; - engine()->addImageProvider(QStringLiteral("thumbnail"), m_thumbnailer); + engine()->addImageProvider(QStringLiteral("thumbnail"), new ThumbnailProvider); + engine()->addImageProvider(QStringLiteral("thumbnailCache"), new ThumbnailCacheProvider); setVisible(false); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setFocusPolicy(Qt::StrongFocus); } TimelineWidget::~TimelineWidget() { delete m_proxy; } void TimelineWidget::updateEffectFavorites() { rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); } void TimelineWidget::updateTransitionFavorites() { rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); } const QStringList TimelineWidget::sortedItems(const QStringList &items, bool isTransition) { QMap sortedItems; for (const QString &effect : items) { sortedItems.insert(m_proxy->getAssetName(effect, isTransition), effect); } return sortedItems.values(); } void TimelineWidget::setModel(const std::shared_ptr &model, MonitorProxy *proxy) { m_sortModel = std::make_unique(this); m_sortModel->setSourceModel(model.get()); m_sortModel->setSortRole(TimelineItemModel::SortRole); m_sortModel->sort(0, Qt::DescendingOrder); m_proxy->setModel(model); rootContext()->setContextProperty("multitrack", m_sortModel.get()); rootContext()->setContextProperty("controller", model.get()); rootContext()->setContextProperty("timeline", m_proxy); rootContext()->setContextProperty("proxy", proxy); // Create a unique id for this timeline to prevent thumbnails // leaking from one project to another because of qml's image caching rootContext()->setContextProperty("documentId", QUuid::createUuid()); rootContext()->setContextProperty("miniFont", QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); // m_transitionProxyModel.get()); // rootContext()->setContextProperty("effectModel", m_effectsProxyModel.get()); rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); rootContext()->setContextProperty("audiorec", pCore->getAudioDevice()); rootContext()->setContextProperty("guidesModel", pCore->projectManager()->current()->getGuideModel().get()); rootContext()->setContextProperty("clipboard", new ClipboardProxy(this)); rootContext()->setContextProperty("smallFont", QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setSource(QUrl(QStringLiteral("qrc:/qml/timeline.qml"))); connect(rootObject(), SIGNAL(mousePosChanged(int)), pCore->window(), SLOT(slotUpdateMousePosition(int))); connect(rootObject(), SIGNAL(zoomIn(bool)), pCore->window(), SLOT(slotZoomIn(bool))); connect(rootObject(), SIGNAL(zoomOut(bool)), pCore->window(), SLOT(slotZoomOut(bool))); connect(rootObject(), SIGNAL(processingDrag(bool)), pCore->window(), SIGNAL(enableUndo(bool))); connect(m_proxy, &TimelineController::seeked, proxy, &MonitorProxy::setPosition); m_proxy->setRoot(rootObject()); setVisible(true); loading = false; m_proxy->checkDuration(); } void TimelineWidget::mousePressEvent(QMouseEvent *event) { emit focusProjectMonitor(); QQuickWidget::mousePressEvent(event); } void TimelineWidget::slotChangeZoom(int value, bool zoomOnMouse) { double pixelScale = QFontMetrics(font()).maxWidth() * 2; m_proxy->setScaleFactorOnMouse(pixelScale / comboScale[value], zoomOnMouse); } void TimelineWidget::slotFitZoom() { QVariant returnedValue; double prevScale = m_proxy->scaleFactor(); QMetaObject::invokeMethod(rootObject(), "fitZoom", Q_RETURN_ARG(QVariant, returnedValue)); double scale = returnedValue.toDouble(); QMetaObject::invokeMethod(rootObject(), "scrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); if (qFuzzyCompare(prevScale, scale)) { scale = m_prevScale; scrollPos = m_scrollPos; } else { m_prevScale = prevScale; m_scrollPos = scrollPos; scrollPos = 0; } m_proxy->setScaleFactorOnMouse(scale, false); // Update zoom slider m_proxy->updateZoom(scale); QMetaObject::invokeMethod(rootObject(), "goToStart", Q_ARG(QVariant, scrollPos)); } Mlt::Tractor *TimelineWidget::tractor() { return m_proxy->tractor(); } TimelineController *TimelineWidget::controller() { return m_proxy; } std::shared_ptr TimelineWidget::model() { return m_proxy->getModel(); } void TimelineWidget::zoneUpdated(const QPoint &zone) { m_proxy->setZone(zone); } void TimelineWidget::setTool(ProjectTool tool) { rootObject()->setProperty("activeTool", (int)tool); } QPoint TimelineWidget::getTracksCount() const { return m_proxy->getTracksCount(); } void TimelineWidget::slotUngrabHack() { // Workaround bug: https://bugreports.qt.io/browse/QTBUG-59044 // https://phabricator.kde.org/D5515 if (quickWindow() && quickWindow()->mouseGrabberItem()) { quickWindow()->mouseGrabberItem()->ungrabMouse(); } } int TimelineWidget::zoomForScale(double value) const { int scale = 100.0 / value; int ix = 13; while (comboScale[ix] > scale && ix > 0) { ix--; } return ix; } void TimelineWidget::focusTimeline() { setFocus(); if (rootObject()) { rootObject()->setFocus(true); } } diff --git a/src/timeline2/view/timelinewidget.h b/src/timeline2/view/timelinewidget.h index 1d474b42d..48467a2a9 100644 --- a/src/timeline2/view/timelinewidget.h +++ b/src/timeline2/view/timelinewidget.h @@ -1,94 +1,93 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TIMELINEWIDGET_H #define TIMELINEWIDGET_H #include "assets/assetlist/model/assetfilter.hpp" #include "assets/assetlist/model/assettreemodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include class ThumbnailProvider; class TimelineController; class QSortFilterProxyModel; class MonitorProxy; class TimelineWidget : public QQuickWidget { Q_OBJECT public: TimelineWidget(QWidget *parent = Q_NULLPTR); ~TimelineWidget() override; /* @brief Sets the model shown by this widget */ void setModel(const std::shared_ptr &model, MonitorProxy *proxy); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); TimelineController *controller(); std::shared_ptr model(); void setTool(ProjectTool tool); QPoint getTracksCount() const; /* @brief calculate zoom level for a scale */ int zoomForScale(double value) const; /* @brief Give keyboard focus to timeline qml */ void focusTimeline(); bool loading; protected: void mousePressEvent(QMouseEvent *event) override; public slots: void slotChangeZoom(int value, bool zoomOnMouse); void slotFitZoom(); void zoneUpdated(const QPoint &zone); /* @brief Favorite effects have changed, reload model for context menu */ void updateEffectFavorites(); /* @brief Favorite transitions have changed, reload model for context menu */ void updateTransitionFavorites(); private slots: void slotUngrabHack(); private: - ThumbnailProvider *m_thumbnailer; TimelineController *m_proxy; static const int comboScale[]; std::shared_ptr m_transitionModel; std::unique_ptr m_transitionProxyModel; std::shared_ptr m_effectsModel; std::unique_ptr m_effectsProxyModel; std::unique_ptr m_sortModel; /* @brief Keep last scale before fit to restore it on second click */ double m_prevScale; /* @brief Keep last scroll position before fit to restore it on second click */ int m_scrollPos; /* @brief Returns an alphabetically sorted list of favorite effects or transitions */ const QStringList sortedItems(const QStringList &items, bool isTransition); signals: void focusProjectMonitor(); void zoneMoved(const QPoint &zone); }; #endif