timeline.qml 92.2 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
import 'Timeline.js' as Logic
6
import com.enums 1.0
7 8 9 10 11 12

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

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

31 32 33 34
    // Zoombar properties
    property double zoomStart: scrollView.visibleArea.xPosition
    property double zoomBarWidth: scrollView.visibleArea.widthRatio

35 36
    FontMetrics {
        id: fontMetrics
37
        font: miniFont
38
    }
39

40 41 42 43
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

44 45 46 47
    function endBinDrag() {
        clipDropArea.processDrop()
    }

48 49 50 51 52
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
53
        return scrollView.contentX
54 55 56
    }

    function goToStart(pos) {
57
        scrollView.contentX = pos
58 59
    }

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

82
    function getActiveTrackStreamPos() {
83 84
        // Return the relative y click position, to display the context menu
        return Logic.getTrackYFromId(timeline.activeTrack) + rulercontainer.height - scrollView.contentY
85
    }
86

87 88
    function updatePalette() {
        root.color = activePalette.window
89
        root.textColor = activePalette.text
90 91
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
92
        // Disable caching for track header icons
93
        root.paletteUnchanged = false
94
    }
95

96
    function moveSelectedTrack(offset) {
97 98 99 100 101 102 103 104 105 106 107
        var newTrack
        if (timeline.activeTrack < 0 ) {
            if (offset <0) {
                newTrack = -2
            } else {
                newTrack = max
            }
        } else {
            var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
            newTrack = cTrack + offset
        }
108 109
        var max = tracksRepeater.count;
        if (newTrack < 0) {
Vincent Pinon's avatar
Vincent Pinon committed
110
            if (showSubtitles && newTrack === -1) {
111 112 113
                timeline.activeTrack = -2
                return
            }
114 115
            newTrack = max - 1;
        } else if (newTrack >= max) {
116 117 118 119
            if (showSubtitles) {
                timeline.activeTrack = -2
                return
            }
120 121
            newTrack = 0;
        }
122
        timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
123 124
    }

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

Pistos Pi's avatar
Pistos Pi committed
159 160 161 162 163 164 165 166 167 168 169
    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,
170
            trackHeaders.height + subtitleTrackHeader.height - tracksArea.height + horZoomBar.height + ruler.height
Pistos Pi's avatar
Pistos Pi committed
171 172 173 174
        )
        scrollView.contentY = Math.max(newScroll, 0)
    }

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

205
    function getMousePos() {
206 207 208
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) / timeline.scaleFactor
        }
209 210 211 212 213
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
214
    }
215 216 217 218 219 220 221 222 223 224
    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;
        }
    }
225

226
    function getScrollPos() {
227
        return scrollView.contentX
228 229 230
    }

    function setScrollPos(pos) {
231
        return scrollView.contentX = pos
232 233
    }

234 235 236 237
    function getCopiedItemId() {
        return copiedClip
    }

238
    function getMouseTrack() {
239 240 241
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return dragProxy.masterObject.trackId
        }
242
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY - subtitleTrack.height)
243 244
    }

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

256 257 258
    function centerViewOnCursor() {
        scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
    }
259

260 261 262 263 264
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
265
        scrollTimer.stop()
266
    }
267

268
    function isDragging() {
269
        return dragInProgress
270
    }
271

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

285
    function endDrag() {
286
        console.log('ENDING DRAG!!!!!!!!!!!!!!!!!!!!!!\n')
287
        dragProxy.draggedItem = -1
288 289
        dragProxy.x = 0
        dragProxy.y = 0
290 291
        dragProxy.width = 0
        dragProxy.height = 0
292
        dragProxy.verticalOffset = 0
293
    }
294
    
295 296 297
    function regainFocus(mousePos) {
        var currentMouseTrack = Logic.getTrackIdFromPos(mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY)
        // Try to find correct item
298 299
        //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)
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        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')
        }
315
    }
316

317 318 319 320 321 322 323 324 325 326
    function getAudioTracksCount(){
        var audioCount = 0;
        for (var i = 0; i < trackHeaderRepeater.count; i++) {
            if(trackHeaderRepeater.itemAt(i).isAudio) {
                audioCount++;
            }
        }
        return audioCount;
    }

327 328
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
329 330 331
        if (track == undefined || track.children == undefined) {
            return undefined
        }
332 333 334
        var container = track.children[0]
        var tentativeClip = undefined
        for (var i = 0 ; i < container.children.length; i++) {
Vincent Pinon's avatar
Vincent Pinon committed
335
            if (container.children[i].children.length === 0 || container.children[i].children[0].children.length === 0) {
336 337 338
                continue
            }
            tentativeClip = container.children[i].children[0].childAt(posx, 1)
Vincent Pinon's avatar
Vincent Pinon committed
339
            if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition === isComposition)) {
340 341 342 343 344
                break
            }
        }
        return tentativeClip
    }
345 346 347 348 349 350
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
351

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

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

414 415 416
    onShowSubtitlesChanged: {
        subtitleTrack.height = showSubtitles? root.baseUnit * 5 : 0
    }
417

418
    //onCurrentTrackChanged: timeline.selection = []
419

420
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
421
        if (root.zoomOnMouse >= 0) {
422
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - getMouseX())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
423
            root.zoomOnMouse = -1
424 425 426
        } else if (root.zoomOnBar >= 0) {
            scrollView.contentX = Math.max(0, root.zoomOnBar * timeline.scaleFactor )
            root.zoomOnBar = -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
427
        } else {
428
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
429
        }
430
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
431
        ruler.adjustStepSize()
432 433 434 435
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
436
    }
437

438
    onConsumerPositionChanged: {
439
        if (root.autoScrolling) Logic.scrollIfNeeded()
440 441
    }

442
    onViewActiveTrackChanged: {
Vincent Pinon's avatar
Vincent Pinon committed
443
        if (timeline.activeTrack === -2) {
444 445 446 447
            // subtitle track
            scrollView.contentY = 0
            return
        }
448
        var tk = Logic.getTrackById(timeline.activeTrack)
449 450 451
        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) {
452
            var newY = Math.min(trackHeaders.height + subtitleTrack.height - scrollView.height, tk.y + tk.height - scrollView.height + subtitleTrack.height)
453 454 455
            if (newY >= 0) {
                scrollView.contentY = newY
            }
456 457 458
        }
    }

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
    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()
            }
        }
    }

484 485 486 487 488 489 490
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
491
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
492
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
493
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
494
                droppedPosition = frame
495
                if (track >= 0 && !controller.isAudioTrack(track)) {
496 497
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
498
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
499 500 501 502 503 504
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
505 506
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
507
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
Vincent Pinon's avatar
Vincent Pinon committed
508
                if (track !== -1) {
509
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
510
                    if (clipBeingDroppedId >= 0){
511 512 513 514
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
515
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, root.snapping)
516
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
517
                    } else if (!controller.isAudioTrack(track)) {
518
                        frame = controller.suggestSnapPoint(frame, root.snapping)
519 520
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
521
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
522
                    }
523 524 525
                }
            }
        }
526
        onExited:{
527 528
            if (clipBeingDroppedId != -1) {
                // If we exit, remove composition
529
                controller.requestItemDeletion(clipBeingDroppedId, false)
530
                clearDropData()
531 532
            }
        }
533 534 535 536 537
        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
538
                controller.requestItemDeletion(clipBeingDroppedId, false)
539
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
540
            }
541
            clearDropData()
542
        }
543
    }
544 545 546
    DropArea {
        //Drop area for bin/clips
        id: clipDropArea
547 548 549
        /** @brief local helper function to handle the insertion of multiple dragged items */
        function insertAndMaybeGroup(track, frame, droppedData) {
            var binIds = droppedData.split(";")
Vincent Pinon's avatar
Vincent Pinon committed
550
            if (binIds.length === 0) {
551 552 553 554
                return -1
            }

            var id = -1
Vincent Pinon's avatar
Vincent Pinon committed
555
            if (binIds.length === 1) {
556
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
557
            } else {
558
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
559 560 561

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
562
                    timeline.selectItems(ids)
563 564 565 566 567 568
                    id = ids[0]
                }
            }
            return id
        }

569 570
        property int fakeFrame: -1
        property int fakeTrack: -1
571 572 573 574
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
575
        keys: 'kdenlive/producerslist'
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
        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()
        }
612
        onEntered: {
613
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
614
                //var track = Logic.getTrackIdFromPos(drag.y)
615 616 617 618 619
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
620
                if (track >= 0  && track < tracksRepeater.count) {
621
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
622
                    droppedPosition = frame
623
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
624
                    //drag.acceptProposedAction()
625
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
626 627 628 629
                    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
630 631
                        frame = controller.adjustFrame(frame, timeline.activeTrack)
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
632
                        if (clipBeingDroppedId > -1) {
633 634 635
                            var moveData = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
                            fakeFrame = moveData[0]
                            fakeTrack = moveData[1]
636 637 638
                        } else {
                            drag.accepted = false
                        }
639
                    }
640
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
641 642 643 644
                } else {
                    drag.accepted = false
                }
            }
645
        }
Nicolas Carion's avatar
Nicolas Carion committed
646
        onExited:{
647 648
            if (clipBeingDroppedId != -1 && drag.y < drag.x) {
                // If we exit on top, remove clip
649
                controller.requestItemDeletion(clipBeingDroppedId, false)
650 651 652
                clearDropData()
            } else {
                // Clip is dropped
Nicolas Carion's avatar
Nicolas Carion committed
653 654
            }
        }
655
        onPositionChanged: {
656
            if (clipBeingMovedId == -1) {
657 658 659 660 661
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
662
                if (track >= 0  && track < tracksRepeater.count) {
663 664
                    //timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
                    var targetTrack = tracksRepeater.itemAt(track).trackInternalId
665
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
666 667 668 669 670
                    if (clipBeingDroppedId > -1) {
                        var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                        fakeFrame = moveData[0]
                        fakeTrack = moveData[1]
                        timeline.activeTrack = fakeTrack
671
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
672
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
673
                    } else {
674
                        frame = controller.suggestSnapPoint(frame, root.snapping)
675
                        if (controller.normalEdit()) {
676 677
                            timeline.activeTrack = targetTrack
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
678 679
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
680 681 682 683 684 685 686
                            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
                            }
687
                        }
688
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
689
                    }
690
                }
Nicolas Carion's avatar
Nicolas Carion committed
691
            }
692 693
        }
        onDropped: {
694
            processDrop()
695 696
        }
    }
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
    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) {
723 724 725 726 727
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
728 729
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
730 731 732
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
                    if (clipBeingDroppedId == -1) {
                        if (controller.normalEdit() == false) {
733 734 735 736 737 738 739 740 741 742
                            // 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
                        }
                    }
                }
            }
        }
        onDropped: {
743
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
            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()
        }
    }
763

764 765
    Row {
        Column {
766
            id: headerContainer
767
            width: headerWidth
768
            z: 1
769
            Item {
770
                // Padding between toolbar and track headers.
771
                width: parent.width
772
                height: ruler.height
773
                Button {
774
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
775