timeline.qml 69.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 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
    property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active
15

16
    signal clipClicked()
17
    signal mousePosChanged(int position)
18
    signal showClipMenu(int cid)
19
    signal showCompositionMenu()
20
    signal showTimelineMenu()
21 22
    signal showRulerMenu()
    signal showHeaderMenu()
23 24
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
25
    signal processingDrag(bool dragging)
26

27 28
    FontMetrics {
        id: fontMetrics
29
        font: miniFont
30
    }
31

32 33 34 35
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

36 37 38 39
    function endBinDrag() {
        clipDropArea.processDrop()
    }

40 41 42 43 44
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
45
        return scrollView.contentX
46 47 48
    }

    function goToStart(pos) {
49
        scrollView.contentX = pos
50 51
    }

52 53 54 55
    function checkDeletion(itemId) {
        if (dragProxy.draggedItem == itemId) {
            endDrag()
        }
56 57 58
        if (itemId == mainItemId) {
            mainItemId = -1
        }
59 60
    }

61 62
    function updatePalette() {
        root.color = activePalette.window
63
        root.textColor = activePalette.text
64 65 66
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
    }
67

68
    function moveSelectedTrack(offset) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
69 70
        var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
        var newTrack = cTrack + offset
71 72 73 74 75 76
        var max = tracksRepeater.count;
        if (newTrack < 0) {
            newTrack = max - 1;
        } else if (newTrack >= max) {
            newTrack = 0;
        }
77
        timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
78 79
    }

80
    function zoomByWheel(wheel) {
81
        if (wheel.modifiers & Qt.AltModifier) {
82
            // Seek to next snap
83 84 85 86 87 88
            if (wheel.angleDelta.x > 0) {
                timeline.triggerAction('monitor_seek_snap_backward')
            } else {
                timeline.triggerAction('monitor_seek_snap_forward')
            }
        } else if (wheel.modifiers & Qt.ControlModifier) {
89
            root.wheelAccumulatedDelta += wheel.angleDelta.y;
90
            // Zoom
91
            if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
92
                root.zoomIn(true);
93 94
                root.wheelAccumulatedDelta = 0;
            } else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
95
                root.zoomOut(true);
96
                root.wheelAccumulatedDelta = 0;
97
            }
98 99
        } else if (wheel.modifiers & Qt.ShiftModifier) {
            // Vertical scroll
100 101
            var newScroll = Math.min(scrollView.contentY - wheel.angleDelta.y, trackHeaders.height - tracksArea.height + scrollView.ScrollBar.horizontal.height + ruler.height)
            scrollView.contentY = Math.max(newScroll, 0)
102
        } else {
103
            // Horizontal scroll
104 105
            var newScroll = Math.min(scrollView.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.ScrollBar.vertical.width))
            scrollView.contentX = Math.max(newScroll, 0)
106
        }
107
        wheel.accepted = true
108 109
    }

110
    function continuousScrolling(x, y) {
111
        // This provides continuous scrolling at the left/right edges.
112
        if (x > scrollView.contentX + scrollView.width - 50) {
113
            scrollTimer.item = clip
114
            scrollTimer.horizontal = 10
115 116
            scrollTimer.start()
        } else if (x < 50) {
117
            scrollView.contentX = 0;
118
            scrollTimer.horizontal = 0
119
            scrollTimer.stop()
120
        } else if (x < scrollView.contentX + 50) {
121
            scrollTimer.item = clip
122
            scrollTimer.horizontal = -10
123 124
            scrollTimer.start()
        } else {
125 126 127 128 129 130 131 132 133 134 135
            if (y > scrollView.contentY + scrollView.height - 50) {
                scrollTimer.vertical = 10
                scrollTimer.start()
            } else if (y - scrollView.contentY < 50) {
                scrollTimer.vertical = -10
                scrollTimer.start()
            } else {
                scrollTimer.vertical = 0
                scrollTimer.horizontal = 0
                scrollTimer.stop()
            }
136 137
        }
    }
138 139 140
    function getTrackYFromId(a_track) {
        return Logic.getTrackYFromId(a_track)
    }
141

142 143 144 145
    function getTrackYFromMltIndex(a_track) {
        return Logic.getTrackYFromMltIndex(a_track)
    }

146 147 148 149
    function getTracksCount() {
        return Logic.getTracksList()
    }

150
    function getMousePos() {
151 152 153 154 155
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
156 157
    }

158
    function getScrollPos() {
159
        return scrollView.contentX
160 161 162
    }

    function setScrollPos(pos) {
163
        return scrollView.contentX = pos
164 165
    }

166 167 168 169
    function getCopiedItemId() {
        return copiedClip
    }

170
    function getMouseTrack() {
171
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY)
172 173
    }

174 175 176
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
177
            col = Qt.tint(col, "#06FF00CC")
178 179
        }
        if (header) {
180
            col = Qt.darker(col, 1.05)
181
        }
182
        return col
183 184
    }

185 186 187 188 189
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
190
        scrollTimer.stop()
191
    }
192

193
    function isDragging() {
194
        return dragInProgress
195
    }
196

197
    function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
198
        dragProxy.x = itemObject.modelStart * timeScale
199
        dragProxy.y = itemCoord.y
200
        dragProxy.width = itemObject.clipDuration * timeScale
201 202 203 204 205 206
        dragProxy.height = itemCoord.height
        dragProxy.masterObject = itemObject
        dragProxy.draggedItem = itemId
        dragProxy.sourceTrack = itemTrack
        dragProxy.sourceFrame = itemPos
        dragProxy.isComposition = isComposition
207
        dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
208 209 210 211 212 213 214
    }
    function endDrag() {
        dragProxy.draggedItem = -1
        dragProxy.x = 0
        dragProxy.y = 0
        dragProxy.width = 0
        dragProxy.height = 0
215
        dragProxy.verticalOffset = 0
216 217
    }

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
        var container = track.children[0]
        var tentativeClip = undefined
        //console.log('TESTING ITMES OK TK: ', tk, ', POS: ', posx, ', CHILREN: ', container.children.length, ', COMPO: ', isComposition)
        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)) {
                //console.log('found item with id: ', tentativeClip.clipId, ' IS COMPO: ', tentativeClip.isComposition)
                break
            }
        }
        return tentativeClip
    }
235 236 237 238 239 240
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
241

242
    property int headerWidth: timeline.headerWidth()
243
    property int activeTool: 0
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
244
    property real baseUnit: fontMetrics.font.pixelSize
245
    property real fontUnit: fontMetrics.font.pointSize
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
246 247
    property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
    property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
248
    property bool autoScrolling: timeline.autoScroll
249
    property int duration: timeline.duration
250 251
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
252
    property color titleColor: timeline.titleColor
Sashmita Raghav's avatar
Sashmita Raghav committed
253
    property color imageColor: timeline.imageColor
254
    property color slideshowColor: timeline.slideshowColor
255
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
256
    property color selectionColor: timeline.selectionColor
257
    property color groupColor: timeline.groupColor
258
    property int mainItemId: -1
259
    property int mainFrame: 0
Nicolas Carion's avatar
Nicolas Carion committed
260
    property int clipBeingDroppedId: -1
261
    property string clipBeingDroppedData
262 263
    property int droppedPosition: -1
    property int droppedTrack: -1
264
    property int clipBeingMovedId: -1
265
    property int consumerPosition: proxy.position
266
    property int spacerGroup: -1
267 268
    property int spacerFrame: -1
    property int spacerClickFrame: -1
269
    property real timeScale: timeline.scaleFactor
270
    property int snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? Math.floor(baseUnit / (timeScale > 3 ? timeScale / 2 : timeScale)) : -1
271
    property var timelineSelection: timeline.selection
272
    property int trackHeight
273
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
274
    property int zoomOnMouse: -1
275
    property int viewActiveTrack: timeline.activeTrack
276 277
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
278
    property bool seekingFinished : proxy.seekFinished
279
    property int scrollMin: scrollView.contentX / timeline.scaleFactor
280
    property int scrollMax: scrollMin + scrollView.contentItem.width / timeline.scaleFactor
281
    property double dar: 16/9
282
    property int collapsedHeight: baseUnit * 1.8
283 284 285 286

    onSeekingFinishedChanged : {
        playhead.opacity = seekingFinished ? 1 : 0.5
    }
287

288
    //onCurrentTrackChanged: timeline.selection = []
289
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
290
        if (root.zoomOnMouse >= 0) {
291
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
292 293
            root.zoomOnMouse = -1
        } else {
294
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
295
        }
296
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
297
        ruler.adjustStepSize()
298 299 300 301
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
302
        console.log('GOT SCALE: ', timeScale, ', BASE: ', baseUnit, ' - SNAPPING: ', snapping)
303
    }
304

305 306 307 308
    onConsumerPositionChanged: {
        if (autoScrolling) Logic.scrollIfNeeded()
    }

309 310
    onViewActiveTrackChanged: {
        var tk = Logic.getTrackById(timeline.activeTrack)
311 312 313
        if (tk.y < scrollView.contentY) {
            scrollView.contentY = Math.max(0, tk.y - scrollView.height / 3)
        } else if (tk.y + tk.height > scrollView.contentY + scrollView.contentItem.height) {
314 315 316 317
            var newY = Math.min(trackHeaders.height - scrollView.height + scrollView.ScrollBar.horizontal.height, tk.y - scrollView.height / 3)
            if (newY >= 0) {
                scrollView.contentY = newY
            }
318 319 320
        }
    }

321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    onActiveToolChanged: {
        if (root.activeTool == 2) {
            // Spacer activated
            endDrag()
        } else if (root.activeTool == 0) {
            var tk = getMouseTrack()
            if (tk < 0) {
                console.log('........ MOUSE OUTSIDE TRAKS\n\n.........')
                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()
            }
        }
    }

347 348 349 350 351 352 353
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
354
            console.log("Trying to drop composition")
355
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
356
                console.log("No clip being moved")
357 358
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY)
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
359
                droppedPosition = frame
360
                if (track >= 0 && !controller.isAudioTrack(track)) {
361
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
362
                    console.log("Trying to insert",track, frame, clipBeingDroppedData)
363
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
364
                    console.log("id",clipBeingDroppedId)
365
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
366 367 368 369 370 371
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
372 373
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
374
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY)
375
                if (track !=-1) {
376
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
377
                    if (clipBeingDroppedId >= 0){
378 379 380 381
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
382
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, root.snapping)
383
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
384
                    } else if (!controller.isAudioTrack(track)) {
385
                        frame = controller.suggestSnapPoint(frame, root.snapping)
386 387
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
388
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
389
                    }
390 391 392
                }
            }
        }
393
        onExited:{
394 395
            if (clipBeingDroppedId != -1) {
                // If we exit, remove composition
396
                controller.requestItemDeletion(clipBeingDroppedId, false)
397
                clearDropData()
398 399
            }
        }
400 401 402 403 404
        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
405
                controller.requestItemDeletion(clipBeingDroppedId, false)
406
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
407
            }
408
            clearDropData()
409
        }
410
    }
411 412 413
    DropArea {
        //Drop area for bin/clips
        id: clipDropArea
414 415 416 417 418 419 420 421 422
        /** @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) {
423
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
424
            } else {
425
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
426 427 428

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
429
                    timeline.selectItems(ids)
430 431 432 433 434 435
                    id = ids[0]
                }
            }
            return id
        }

436 437
        property int fakeFrame: -1
        property int fakeTrack: -1
438 439 440 441
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
442
        keys: 'kdenlive/producerslist'
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
        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()
        }
479
        onEntered: {
480
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
481
                //var track = Logic.getTrackIdFromPos(drag.y)
482
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
483
                if (track >= 0  && track < tracksRepeater.count) {
484
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
485
                    droppedPosition = frame
486
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
487
                    //drag.acceptProposedAction()
488
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
489
                    console.log('dropped data: ', clipBeingDroppedData)
490 491 492 493 494
                    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
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
495
                        if (clipBeingDroppedId > -1) {
496
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
497 498 499 500
                            fakeTrack = timeline.activeTrack
                        } else {
                            drag.accepted = false
                        }
501
                    }
502
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
503 504 505 506
                } else {
                    drag.accepted = false
                }
            }
507
        }
Nicolas Carion's avatar
Nicolas Carion committed
508
        onExited:{
509 510
            if (clipBeingDroppedId != -1 && drag.y < drag.x) {
                // If we exit on top, remove clip
511
                controller.requestItemDeletion(clipBeingDroppedId, false)
512 513 514
                clearDropData()
            } else {
                // Clip is dropped
Nicolas Carion's avatar
Nicolas Carion committed
515 516
            }
        }
517
        onPositionChanged: {
518
            if (clipBeingMovedId == -1) {
519
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
520
                if (track >= 0  && track < tracksRepeater.count) {
521
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
522
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
523
                    if (clipBeingDroppedId >= 0) {
524
                        fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
525 526
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
527
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
528
                    } else {
529
                        frame = controller.suggestSnapPoint(frame, root.snapping)
530 531 532 533 534
                        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)
535
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
536 537
                            fakeTrack = timeline.activeTrack
                        }
538
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
539
                    }
540
                }
Nicolas Carion's avatar
Nicolas Carion committed
541
            }
542 543
        }
        onDropped: {
544
            processDrop()
545 546
        }
    }
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
    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) {
573
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
574 575
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
576
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
577 578 579 580
                    if (clipBeingDroppedId >= 0) {
                        //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
581
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
582
                    } else {
583
                        frame = controller.suggestSnapPoint(frame, root.snapping)
584 585 586 587 588 589 590 591
                        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
                        }
592
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
593 594 595 596 597
                    }
                }
            }
        }
        onDropped: {
598
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
            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()
        }
    }
618

619 620
    Row {
        Column {
621
            id: headerContainer
622
            width: headerWidth
623
            z: 1
624
            Item {
625
                // Padding between toolbar and track headers.
626
                width: parent.width
627
                height: ruler.height
628
                Button {
629
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
630 631 632
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
633 634 635 636
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
637 638 639 640
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
641 642 643 644
                    onClicked: {
                        timeline.showMasterEffects()
                    }
                }
645 646 647
            }
            Flickable {
                // Non-slider scroll area for the track headers.
648
                id: headerFlick
649
                contentY: scrollView.contentY
650 651 652
                width: parent.width
                y: ruler.height
                height: root.height - ruler.height
653
                interactive: false
654
                clip: true
655

656 657 658 659 660
                MouseArea {
                    width: trackHeaders.width
                    height: trackHeaders.height
                    acceptedButtons: Qt.NoButton
                    onWheel: {
661 662
                        var newScroll = Math.min(scrollView.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.ScrollBar.horizontal.height + ruler.height)
                        scrollView.contentY = Math.max(newScroll, 0)
663 664
                    }
                }
665 666
                Column {
                    id: trackHeaders
667
                    spacing: 0
668 669 670 671 672
                    Repeater {
                        id: trackHeaderRepeater
                        model: multitrack
                        TrackHead {
                            trackName: model.name
673
                            thumbsFormat: model.thumbsFormat
674
                            trackTag: model.trackTag
675
                            isDisabled: model.disabled
676 677
                            isComposite: model.composite
                            isLocked: model.locked
678
                            isActive: model.trackActive
679
                            isAudio: model.audio
680
                            showAudioRecord: model.audioRecord
681 682
                            effectNames: model.effectNames
                            isStackEnabled: model.isStackEnabled
683
                            width: headerWidth
684
                            current: item === timeline.activeTrack
685
                            trackId: item
686
                            height: model.trackHeight
687
                            onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
688
                            collapsed: height <= root.collapsedHeight
689
                            onMyTrackHeightChanged: {
690
                                collapsed = myTrackHeight <= root.collapsedHeight
691 692
                                if (!collapsed) {
                                    controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
693 694
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
                                } else {
695
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", root.collapsedHeight)
696
                                }
697 698
                                // hack: change property to trigger transition adjustment
                                root.trackHeight = root.trackHeight === 1 ? 0 : 1
699
                            }
700 701 702 703 704 705
                        }
                    }
                }
                Column {
                    id: trackHeadersResizer
                    spacing: 0
706
                    width: root.baseUnit / 2
707 708 709
                    Rectangle {
                        id: resizer
                        height: trackHeaders.height
710
                        width: root.baseUnit / 2
711 712 713 714 715 716 717 718 719 720 721 722 723
                        x: root.headerWidth - 2
                        color: 'red'
                        opacity: 0
                        Drag.active: headerMouseArea.drag.active
                        Drag.proposedAction: Qt.MoveAction

                        MouseArea {
                            id: headerMouseArea
                            anchors.fill: parent
                            hoverEnabled: true
                            cursorShape: Qt.SizeHorCursor
                            drag.target: parent
                            drag.axis: Drag.XAxis
724
                            drag.minimumX: 2 * baseUnit
725 726 727 728 729
                            property double startX
                            property double originalX
                            drag.smoothed: false

                            onPressed: {
730
                                root.autoScrolling = false
731 732
                            }
                            onReleased: {
733
                                root.autoScrolling = timeline.autoScroll
734 735 736 737 738 739 740 741 742 743 744 745
                                parent.opacity = 0
                            }
                            onEntered: parent.opacity = 0.5
                            onExited: parent.opacity = 0
                            onPositionChanged: {
                                if (mouse.buttons === Qt.LeftButton) {
                                    parent.opacity = 0.5
                                    headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
                                    timeline.setHeaderWidth(headerWidth)
                                }
                            }
                        }
746 747 748 749 750 751
                    }
                }
            }
        }
        MouseArea {
            id: tracksArea
752 753
            property real clickX
            property real clickY
754
            width: root.width - root.headerWidth
755
            height: root.height
756
            x: root.headerWidth
757
            property bool shiftPress: false
758 759
            // This provides continuous scrubbing and scimming at the left/right edges.
            hoverEnabled: true
760 761
            preventStealing: true
            acceptedButtons: Qt.AllButtons
762
            cursorShape: root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
763
            onWheel: {
764 765 766 767 768 769 770 771 772
                if (wheel.modifiers & Qt.AltModifier) {
                    // Alt + wheel = seek to next snap point
                    if (wheel.angleDelta.x > 0) {
                        timeline.triggerAction('monitor_seek_snap_backward')
                    } else {
                        timeline.triggerAction('monitor_seek_snap_forward')
                    }
                } else {
                    var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
773
                    proxy.position = wheel.angleDelta.y > 0 ? Math.max(root.consumerPosition - delta, 0) : Math.min(root.consumerPosition + delta, timeline.fullDuration - 1)
774
                }
775
            }
776
            onPressed: {
777
                focus = true
778 779
                shiftPress = (mouse.modifiers & Qt.ShiftModifier) && (mouse.y > ruler.height)
                if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && (mouse.modifiers & Qt.ControlModifier) && !shiftPress)) {
780 781 782 783
                    clickX = mouseX
                    clickY = mouseY
                    return
                }