timeline.qml 92.4 KB
Newer Older
1 2 3
import QtQuick 2.11
import QtQml.Models 2.11
import QtQuick.Controls 2.4
4
import Kdenlive.Controls 1.0
5 6 7 8 9 10 11
import 'Timeline.js' as Logic

Rectangle {
    id: root
    objectName: "timelineview"
    SystemPalette { id: activePalette }
    color: activePalette.window
12
    property bool validMenu: false
13
    property color textColor: activePalette.text
14 15
    property var groupTrimData
    property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active || groupTrimData != undefined
16

17
    signal clipClicked()
18
    signal mousePosChanged(int position)
19
    signal showClipMenu(int cid)
20
    signal showCompositionMenu()
21
    signal showTimelineMenu()
22 23
    signal showRulerMenu()
    signal showHeaderMenu()
24
    signal showTargetMenu(int ix)
25 26
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
27
    signal processingDrag(bool dragging)
Sashmita Raghav's avatar
Sashmita Raghav committed
28
    signal showSubtitleClipMenu()
29

30 31
    FontMetrics {
        id: fontMetrics
32
        font: miniFont
33
    }
34

35 36 37 38
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

39 40 41 42
    function endBinDrag() {
        clipDropArea.processDrop()
    }

43 44 45 46 47
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
48
        return scrollView.contentX
49 50 51
    }

    function goToStart(pos) {
52
        scrollView.contentX = pos
53 54
    }

55 56 57 58 59 60 61 62
    function switchSubtitleTrack() {
        if (subtitleTrack.height > root.collapsedHeight) {
            subtitleTrack.height = root.collapsedHeight
        } else {
            subtitleTrack.height = 5 * root.baseUnit
        }
    }
    
63 64 65 66 67
    function highlightSub(ix) {
        var currentSub = subtitlesRepeater.itemAt(ix)
        currentSub.editText()
    }
    
68 69 70 71
    function checkDeletion(itemId) {
        if (dragProxy.draggedItem == itemId) {
            endDrag()
        }
72 73 74
        if (itemId == mainItemId) {
            mainItemId = -1
        }
75
    }
Pistos Pi's avatar
Pistos Pi committed
76

77
    function getActiveTrackStreamPos() {
78 79
        // Return the relative y click position, to display the context menu
        return Logic.getTrackYFromId(timeline.activeTrack) + rulercontainer.height - scrollView.contentY
80
    }
81

82 83
    function updatePalette() {
        root.color = activePalette.window
84
        root.textColor = activePalette.text
85 86
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
87
        // Disable caching for track header icons
88
        root.paletteUnchanged = false
89
    }
90

91
    function moveSelectedTrack(offset) {
92 93 94 95 96 97 98 99 100 101 102
        var newTrack
        if (timeline.activeTrack < 0 ) {
            if (offset <0) {
                newTrack = -2
            } else {
                newTrack = max
            }
        } else {
            var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
            newTrack = cTrack + offset
        }
103 104
        var max = tracksRepeater.count;
        if (newTrack < 0) {
105 106 107 108
            if (showSubtitles && newTrack == -1) {
                timeline.activeTrack = -2
                return
            }
109 110
            newTrack = max - 1;
        } else if (newTrack >= max) {
111 112 113 114
            if (showSubtitles) {
                timeline.activeTrack = -2
                return
            }
115 116
            newTrack = 0;
        }
117
        timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
118 119
    }

120
    function zoomByWheel(wheel) {
121
        if (wheel.modifiers & Qt.AltModifier) {
122
            // Seek to next snap
123 124 125 126 127 128
            if (wheel.angleDelta.x > 0) {
                timeline.triggerAction('monitor_seek_snap_backward')
            } else {
                timeline.triggerAction('monitor_seek_snap_forward')
            }
        } else if (wheel.modifiers & Qt.ControlModifier) {
129
            root.wheelAccumulatedDelta += wheel.angleDelta.y;
130
            // Zoom
131
            if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
132
                root.zoomIn(true);
133 134
                root.wheelAccumulatedDelta = 0;
            } else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
135
                root.zoomOut(true);
136
                root.wheelAccumulatedDelta = 0;
137
            }
138
        } else if (wheel.modifiers & Qt.ShiftModifier) {
Pistos Pi's avatar
Pistos Pi committed
139
            if (scrollVertically) {
Pistos Pi's avatar
Pistos Pi committed
140
                horizontalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
141
            } else {
Pistos Pi's avatar
Pistos Pi committed
142
                verticalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
143
            }
144
        } else {
Pistos Pi's avatar
Pistos Pi committed
145
            if (scrollVertically) {
Pistos Pi's avatar
Pistos Pi committed
146
                verticalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
147
            } else {
Pistos Pi's avatar
Pistos Pi committed
148
                horizontalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
149
            }
150
        }
151
        wheel.accepted = true
152 153
    }

Pistos Pi's avatar
Pistos Pi committed
154 155 156 157 158 159 160 161 162 163 164
    function horizontalScroll(wheel) {
        var newScroll = Math.min(
          scrollView.contentX - wheel.angleDelta.y,
          timeline.fullDuration * root.timeScale - scrollView.width
        )
        scrollView.contentX = Math.max(newScroll, 0)
    }

    function verticalScroll(wheel) {
        var newScroll = Math.min(
            scrollView.contentY - wheel.angleDelta.y,
165
            trackHeaders.height + subtitleTrackHeader.height - tracksArea.height + horScroll.height + ruler.height
Pistos Pi's avatar
Pistos Pi committed
166 167 168 169
        )
        scrollView.contentY = Math.max(newScroll, 0)
    }

170
    function continuousScrolling(x, y) {
171
        // This provides continuous scrolling at the left/right edges.
172
        if (x > scrollView.contentX + scrollView.width - root.baseUnit * 3) {
173
            scrollTimer.horizontal = root.baseUnit
174 175
            scrollTimer.start()
        } else if (x < 50) {
176
            scrollView.contentX = 0;
177
            scrollTimer.horizontal = 0
178
            scrollTimer.stop()
179
        } else if (x < scrollView.contentX + root.baseUnit * 3) {
180
            scrollTimer.horizontal = -root.baseUnit
181 182
            scrollTimer.start()
        } else {
183 184
            if (y > scrollView.contentY + scrollView.height + ruler.height - root.baseUnit) {
                scrollTimer.vertical = root.baseUnit
185
                scrollTimer.horizontal = 0
186
                scrollTimer.start()
187 188
            } else if (scrollView.contentY > 0 && (y - (scrollView.contentY + ruler.height ) < root.baseUnit)) {
                scrollTimer.vertical = -root.baseUnit
189
                scrollTimer.horizontal = 0
190 191
                scrollTimer.start()
            } else {
192
                console.log('TRIGGERING VERTICAL STOP: ', y, ', SCROLL: ',scrollView.contentY, 'SCH:', scrollView.height)
193 194 195 196
                scrollTimer.vertical = 0
                scrollTimer.horizontal = 0
                scrollTimer.stop()
            }
197 198
        }
    }
199

200
    function getMousePos() {
201 202 203
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) / timeline.scaleFactor
        }
204 205 206 207 208
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
209
    }
210 211 212 213 214 215 216 217 218 219
    function getMouseX() {
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) - scrollView.contentX
        }
        if (tracksArea.containsMouse) {
            return tracksArea.mouseX
        } else {
            return -1;
        }
    }
220

221
    function getScrollPos() {
222
        return scrollView.contentX
223 224 225
    }

    function setScrollPos(pos) {
226
        return scrollView.contentX = pos
227 228
    }

229 230 231 232
    function getCopiedItemId() {
        return copiedClip
    }

233
    function getMouseTrack() {
234 235 236
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return dragProxy.masterObject.trackId
        }
237
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY - subtitleTrack.height)
238 239
    }

240 241 242
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
243
            col = Qt.tint(col, "#06FF00CC")
244 245
        }
        if (header) {
246
            col = Qt.darker(col, 1.05)
247
        }
248
        return col
249
    }
Pistos Pi's avatar
Pistos Pi committed
250

251 252 253
    function centerViewOnCursor() {
        scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
    }
254

255 256 257 258 259
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
260
        scrollTimer.stop()
261
    }
262

263
    function isDragging() {
264
        return dragInProgress
265
    }
266

267
    function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
268
        dragProxy.x = itemObject.modelStart * timeScale
269
        dragProxy.y = itemCoord.y
270
        dragProxy.width = itemObject.clipDuration * timeScale
271 272 273 274 275 276
        dragProxy.height = itemCoord.height
        dragProxy.masterObject = itemObject
        dragProxy.draggedItem = itemId
        dragProxy.sourceTrack = itemTrack
        dragProxy.sourceFrame = itemPos
        dragProxy.isComposition = isComposition
277
        dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
278
    }
279

280
    function endDrag() {
281
        console.log('ENDING DRAG!!!!!!!!!!!!!!!!!!!!!!\n')
282
        dragProxy.draggedItem = -1
283 284
        dragProxy.x = 0
        dragProxy.y = 0
285 286
        dragProxy.width = 0
        dragProxy.height = 0
287
        dragProxy.verticalOffset = 0
288
    }
289
    
290 291 292
    function regainFocus(mousePos) {
        var currentMouseTrack = Logic.getTrackIdFromPos(mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY)
        // Try to find correct item
293 294
        //console.log('checking item on TK: ', currentMouseTrack, ' AT: XPOS', (mousePos.x - trackHeaders.width), ', SCOLL:', scrollView.contentX, ', RES: ', ((mousePos.x - trackHeaders.width + scrollView.contentX) / timeline.scaleFactor), ' SCROLL POS: ', (mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY))
        var tentativeClip = getItemAtPos(currentMouseTrack, mousePos.x - trackHeaders.width + scrollView.contentX, dragProxy.isComposition)
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
        if (tentativeClip && tentativeClip.clipId) {
            dragProxy.draggedItem = tentativeClip.clipId
            var tk = controller.getItemTrackId(tentativeClip.clipId)
            dragProxy.x = tentativeClip.x
            dragProxy.y = tentativeClip.y + Logic.getTrackYFromId(tk)
            dragProxy.width = tentativeClip.width
            dragProxy.height = tentativeClip.height
            dragProxy.masterObject = tentativeClip
            dragProxy.sourceTrack = tk
            console.log('missing item', tentativeClip.clipId, ', COORDS: ', tentativeClip.x, 'x', tentativeClip.y, ', TK id: ', tk, ', TKY: ', Logic.getTrackYFromId(tk))
            dragProxy.sourceFrame = tentativeClip.modelStart
            dragProxy.isComposition = tentativeClip.isComposition
        } else {
            console.log('item not found')
        }
310
    }
311

312 313 314 315 316 317 318 319 320 321
    function getAudioTracksCount(){
        var audioCount = 0;
        for (var i = 0; i < trackHeaderRepeater.count; i++) {
            if(trackHeaderRepeater.itemAt(i).isAudio) {
                audioCount++;
            }
        }
        return audioCount;
    }

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
        var container = track.children[0]
        var tentativeClip = undefined
        for (var i = 0 ; i < container.children.length; i++) {
            if (container.children[i].children.length == 0 || container.children[i].children[0].children.length == 0) {
                continue
            }
            tentativeClip = container.children[i].children[0].childAt(posx, 1)
            if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition == isComposition)) {
                break
            }
        }
        return tentativeClip
    }
337 338 339 340 341 342
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
343

344
    property int activeTool: 0
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
345
    property real baseUnit: fontMetrics.font.pixelSize
346
    property real fontUnit: fontMetrics.font.pointSize
347 348 349
    property int collapsedHeight: Math.max(28, baseUnit * 1.8)
    property int minHeaderWidth: 6 * collapsedHeight
    property int headerWidth: Math.max(minHeaderWidth, timeline.headerWidth())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
350
    property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
351
    property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.5)
352
    property bool autoScrolling: timeline.autoScroll
353
    property int duration: timeline.duration
354 355
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
356
    property color titleColor: timeline.titleColor
Sashmita Raghav's avatar
Sashmita Raghav committed
357
    property color imageColor: timeline.imageColor
358
    property color slideshowColor: timeline.slideshowColor
359
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
360
    property color selectionColor: timeline.selectionColor
361
    property color groupColor: timeline.groupColor
362 363
    property color thumbColor1: timeline.thumbColor1
    property color thumbColor2: timeline.thumbColor2
364
    property int mainItemId: -1
365
    property int mainFrame: -1
Nicolas Carion's avatar
Nicolas Carion committed
366
    property int clipBeingDroppedId: -1
367
    property string clipBeingDroppedData
368 369
    property int droppedPosition: -1
    property int droppedTrack: -1
370
    property int clipBeingMovedId: -1
371
    property int consumerPosition: proxy.position
372
    property int spacerGroup: -1
373
    property int spacerTrack: -1
374
    property int spacerFrame: -1
375
    property int finalSpacerFrame: -1
376
    property int spacerClickFrame: -1
377
    property bool spacerGuides: false
378
    property real timeScale: timeline.scaleFactor
379
    property int snapping: (timeline.snap && (timeline.scaleFactor < 2 * baseUnit)) ? Math.floor(baseUnit / (timeline.scaleFactor > 3 ? timeline.scaleFactor / 2 : timeline.scaleFactor)) : -1
380
    property var timelineSelection: timeline.selection
381
    property int selectedMix: timeline.selectedMix
382
    property int trackHeight
383
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
384
    property int zoomOnMouse: -1
385
    property int viewActiveTrack: timeline.activeTrack
386 387
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
388
    property bool seekingFinished : proxy.seekFinished
389
    property int scrollMin: scrollView.contentX / timeline.scaleFactor
390
    property int scrollMax: scrollMin + scrollView.contentItem.width / timeline.scaleFactor
391
    property double dar: 16/9
392
    property bool paletteUnchanged: true
393
    property int maxLabelWidth: 20 * root.baseUnit * Math.sqrt(root.timeScale)
394
    property bool showSubtitles: false
395 396
    property bool subtitlesLocked: timeline.subtitlesLocked
    property bool subtitlesDisabled: timeline.subtitlesDisabled
397
    property int trackTagWidth: fontMetrics.boundingRect("M").width * ((getAudioTracksCount() > 9) || (trackHeaderRepeater.count - getAudioTracksCount() > 9)  ? 3 : 2)
Pistos Pi's avatar
Pistos Pi committed
398
    property bool scrollVertically: timeline.scrollVertically
399 400 401 402

    onSeekingFinishedChanged : {
        playhead.opacity = seekingFinished ? 1 : 0.5
    }
Pistos Pi's avatar
Pistos Pi committed
403

404 405 406
    onShowSubtitlesChanged: {
        subtitleTrack.height = showSubtitles? root.baseUnit * 5 : 0
    }
407

408
    //onCurrentTrackChanged: timeline.selection = []
409
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
410
        if (root.zoomOnMouse >= 0) {
411
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - getMouseX())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
412 413
            root.zoomOnMouse = -1
        } else {
414
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
415
        }
416
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
417
        ruler.adjustStepSize()
418 419 420 421
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
422
    }
423

424
    onConsumerPositionChanged: {
425
        if (root.autoScrolling) Logic.scrollIfNeeded()
426 427
    }

428 429
    onViewActiveTrackChanged: {
        var tk = Logic.getTrackById(timeline.activeTrack)
430 431 432 433
        if (tk.y + subtitleTrack.height < scrollView.contentY) {
            scrollView.contentY = Math.max(0, tk.y + subtitleTrack.height)
        } else if (tk.y + tk.height + subtitleTrack.height > scrollView.contentY + scrollView.height) {
            var newY = Math.min(trackHeaders.height + subtitleTrack.height - scrollView.height + scrollView.ScrollBar.horizontal.height, tk.y + tk.height - scrollView.height + subtitleTrack.height)
434 435 436
            if (newY >= 0) {
                scrollView.contentY = newY
            }
437 438 439
        }
    }

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
    onActiveToolChanged: {
        if (root.activeTool == 2) {
            // Spacer activated
            endDrag()
        } else if (root.activeTool == 0) {
            var tk = getMouseTrack()
            if (tk < 0) {
                return
            }
            var pos = getMousePos() * timeline.scaleFactor
            var sourceTrack = Logic.getTrackById(tk)
            var allowComposition = tracksArea.mouseY- sourceTrack.y > sourceTrack.height / 2
            var tentativeItem = undefined
            if (allowComposition) {
                tentativeItem = getItemAtPos(tk, pos, true)
            }
            if (!tentativeItem) {
                tentativeItem = getItemAtPos(tk, pos, false)
            }
            if (tentativeItem) {
                tentativeItem.updateDrag()
            }
        }
    }

465 466 467 468 469 470 471
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
472
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
473
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
474
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
475
                droppedPosition = frame
476
                if (track >= 0 && !controller.isAudioTrack(track)) {
477 478
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
479
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
480 481 482 483 484 485
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
486 487
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
488
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
489
                if (track !=-1) {
490
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
491
                    if (clipBeingDroppedId >= 0){
492 493 494 495
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
496
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, root.snapping)
497
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
498
                    } else if (!controller.isAudioTrack(track)) {
499
                        frame = controller.suggestSnapPoint(frame, root.snapping)
500 501
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
502
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
503
                    }
504 505 506
                }
            }
        }
507
        onExited:{
508 509
            if (clipBeingDroppedId != -1) {
                // If we exit, remove composition
510
                controller.requestItemDeletion(clipBeingDroppedId, false)
511
                clearDropData()
512 513
            }
        }
514 515 516 517 518
        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
519
                controller.requestItemDeletion(clipBeingDroppedId, false)
520
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
521
            }
522
            clearDropData()
523
        }
524
    }
525 526 527
    DropArea {
        //Drop area for bin/clips
        id: clipDropArea
528 529 530 531 532 533 534 535 536
        /** @brief local helper function to handle the insertion of multiple dragged items */
        function insertAndMaybeGroup(track, frame, droppedData) {
            var binIds = droppedData.split(";")
            if (binIds.length == 0) {
                return -1
            }

            var id = -1
            if (binIds.length == 1) {
537
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
538
            } else {
539
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
540 541 542

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
543
                    timeline.selectItems(ids)
544 545 546 547 548 549
                    id = ids[0]
                }
            }
            return id
        }

550 551
        property int fakeFrame: -1
        property int fakeTrack: -1
552 553 554 555
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
556
        keys: 'kdenlive/producerslist'
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
        function processDrop()
        {
            // Process the drop event, useful if drop event happens outside of drop area
            if (clipBeingDroppedId != -1) {
                var frame = controller.getClipPosition(clipBeingDroppedId)
                var track = controller.getClipTrackId(clipBeingDroppedId)
                if (!controller.normalEdit()) {
                    frame = fakeFrame
                    track = fakeTrack
                }
                /* We simulate insertion at the final position so that stored undo has correct value
                 * NOTE: even if dropping multiple clips, requesting the deletion of the first one is
                 * enough as internally it will request the group deletion
                 */
                controller.requestItemDeletion(clipBeingDroppedId, false)

                var binIds = clipBeingDroppedData.split(";")
                if (binIds.length == 1) {
                    if (controller.normalEdit()) {
                        timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
                    } else {
                        timeline.insertClipZone(clipBeingDroppedData, track, frame)
                    }
                } else {
                    if (controller.normalEdit()) {
                        timeline.insertClips(track, frame, binIds, true, true)
                    } else {
                        // TODO
                        console.log('multiple clips insert/overwrite not supported yet')
                    }
                }
                fakeTrack = -1
                fakeFrame = -1
            }
            clearDropData()
        }
593
        onEntered: {
594
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
595
                //var track = Logic.getTrackIdFromPos(drag.y)
596 597 598 599 600
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
601
                if (track >= 0  && track < tracksRepeater.count) {
602
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
603
                    droppedPosition = frame
604
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
605
                    //drag.acceptProposedAction()
606
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
607 608 609 610
                    if (controller.normalEdit()) {
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
                    } else {
                        // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
611 612
                        frame = controller.adjustFrame(frame, timeline.activeTrack)
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
613
                        if (clipBeingDroppedId > -1) {
614 615 616
                            var moveData = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
                            fakeFrame = moveData[0]
                            fakeTrack = moveData[1]
617 618 619
                        } else {
                            drag.accepted = false
                        }
620
                    }
621
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
622 623 624 625
                } else {
                    drag.accepted = false
                }
            }
626
        }
Nicolas Carion's avatar
Nicolas Carion committed
627
        onExited:{
628 629
            if (clipBeingDroppedId != -1 && drag.y < drag.x) {
                // If we exit on top, remove clip
630
                controller.requestItemDeletion(clipBeingDroppedId, false)
631 632 633
                clearDropData()
            } else {
                // Clip is dropped
Nicolas Carion's avatar
Nicolas Carion committed
634 635
            }
        }
636
        onPositionChanged: {
637
            if (clipBeingMovedId == -1) {
638 639 640 641 642
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
643
                if (track >= 0  && track < tracksRepeater.count) {
644 645
                    //timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
                    var targetTrack = tracksRepeater.itemAt(track).trackInternalId
646
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
647 648 649 650 651
                    if (clipBeingDroppedId > -1) {
                        var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                        fakeFrame = moveData[0]
                        fakeTrack = moveData[1]
                        timeline.activeTrack = fakeTrack
652
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
653
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
654
                    } else {
655
                        frame = controller.suggestSnapPoint(frame, root.snapping)
656
                        if (controller.normalEdit()) {
657 658
                            timeline.activeTrack = targetTrack
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
659 660
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
661 662 663 664 665 666 667
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, timeline.fullDuration, clipBeingDroppedData)
                            if (clipBeingDroppedId > -1) {
                                var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                                fakeFrame = moveData[0]
                                fakeTrack = moveData[1]
                                timeline.activeTrack = fakeTrack
                            }
668
                        }
669
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
670
                    }
671
                }
Nicolas Carion's avatar
Nicolas Carion committed
672
            }
673 674
        }
        onDropped: {
675
            processDrop()
676 677
        }
    }
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
    DropArea { //Drop area for urls (direct drop from file manager)
        /** @brief local helper function to handle the insertion of multiple dragged items */
        property int fakeFrame: -1
        property int fakeTrack: -1
        property var droppedUrls: []
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'text/uri-list'
        onEntered: {
            drag.accepted = true
            droppedUrls.length = 0
            for(var i in drag.urls){
                var url = drag.urls[i]
                droppedUrls.push(Qt.resolvedUrl(url))
            }
        }
        onExited:{
            if (clipBeingDroppedId != -1) {
                controller.requestItemDeletion(clipBeingDroppedId, false)
            }
            clearDropData()
        }
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
704 705 706 707 708
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
709 710
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
711
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
712 713
                    if (clipBeingDroppedId >= 0) {
                        //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
714
                        //fakeTrack = timeline.activeTrack
715
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
716
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
717
                    } else {
718
                        frame = controller.suggestSnapPoint(frame, root.snapping)
719 720 721 722 723 724 725 726
                        if (controller.normalEdit()) {
                            //clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
                            //clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
                            //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
                            fakeTrack = timeline.activeTrack
                        }
727
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
728 729 730 731 732
                    }
                }
            }
        }
        onDropped: {
733
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
            var track = timeline.activeTrack
            //var binIds = clipBeingDroppedData.split(";")
            //if (binIds.length == 1) {
                if (controller.normalEdit()) {
                    timeline.urlDropped(droppedUrls, frame, track)
                } else {
                    //timeline.insertClipZone(clipBeingDroppedData, track, frame)
                }
            /*} else {
                if (controller.normalEdit()) {
                    timeline.insertClips(track, frame, binIds, true, true)
                } else {
                    // TODO
                    console.log('multiple clips insert/overwrite not supported yet')
                }
            }*/
            clearDropData()
        }
    }
753

754 755
    Row {
        Column {
756
            id: headerContainer
757
            width: headerWidth
758
            z: 1
759
            Item {
760
                // Padding between toolbar and track headers.
761
                width: parent.width
762
                height: ruler.height
763
                Button {
764
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
765 766 767
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
768 769 770 771
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
772 773 774 775
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
776 777 778
                    onClicked: {
                        timeline.showMasterEffects()
                    }
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
                    DropArea { //Drop area for tracks
                        anchors.fill: parent
                        keys: 'kdenlive/effect'
                        property string dropData
                        property string dropSource
                        property int dropRow: -1
                        onEntered: {
                            dropData = drag.getDataAsString('kdenlive/effect')
                            dropSource = drag.getDataAsString('kdenlive/effectsource')
                        }
                        onDropped: {
                            console.log("Add effect: ", dropData)
                            if (dropSource == '') {
                                // drop from effects list
                                controller.addTrackEffect(-1, dropData);
                            } else {
                                controller.copyTrackEffect(-1, dropSource);
                            }
                            dropSource = ''
                            dropRow = -1
                            drag.acceptProposedAction
                        }
                    }
802
                }
803 804 805
            }
            Flickable {
                // Non-slider scroll area for the track headers.